From cd950cef199e1f36fcb253ac94095b5a22c8e691 Mon Sep 17 00:00:00 2001 From: Johny Muffin Date: Thu, 5 Jan 2023 04:01:12 +1000 Subject: [PATCH] Version 1.0.0 --- .gitignore | 222 +++++++++ .../johnymuffin/beta/jdetector/JDetector.java | 65 +++ .../johnymuffin/beta/jdetector/JIPCache.java | 90 ++++ .../johnymuffin/beta/jdetector/JListener.java | 169 +++++++ .../johnymuffin/beta/jdetector/JSettings.java | 81 +++ .../jdetector/utils/BetaEvolutionsUtils.java | 463 ++++++++++++++++++ src/plugin.yml | 6 + 7 files changed, 1096 insertions(+) create mode 100644 .gitignore create mode 100644 src/com/johnymuffin/beta/jdetector/JDetector.java create mode 100644 src/com/johnymuffin/beta/jdetector/JIPCache.java create mode 100644 src/com/johnymuffin/beta/jdetector/JListener.java create mode 100644 src/com/johnymuffin/beta/jdetector/JSettings.java create mode 100644 src/com/johnymuffin/beta/jdetector/utils/BetaEvolutionsUtils.java create mode 100644 src/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a0543a --- /dev/null +++ b/.gitignore @@ -0,0 +1,222 @@ + +# Created by https://www.gitignore.io/api/java,intellij,intellij+all,intellij+iml +# Edit at https://www.gitignore.io/?templates=java,intellij,intellij+all,intellij+iml + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### Intellij+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Intellij+iml ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### Intellij+iml Patch ### +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# End of https://www.gitignore.io/api/java,intellij,intellij+all,intellij+iml diff --git a/src/com/johnymuffin/beta/jdetector/JDetector.java b/src/com/johnymuffin/beta/jdetector/JDetector.java new file mode 100644 index 0000000..988939a --- /dev/null +++ b/src/com/johnymuffin/beta/jdetector/JDetector.java @@ -0,0 +1,65 @@ +package com.johnymuffin.beta.jdetector; + +import com.johnymuffin.beta.jdetector.utils.BetaEvolutionsUtils; +import org.bukkit.Bukkit; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class JDetector extends JavaPlugin { + + //Basic Plugin Info + private static JDetector plugin; + private Logger log; + private String pluginName; + private PluginDescriptionFile pdf; + + //IPCache + private JIPCache JIPCache; + + private JSettings jSettings; + + private HashMap betaEVOVerificationResults = new HashMap<>(); + + + @Override + public void onEnable() { + plugin = this; + log = this.getServer().getLogger(); + pdf = this.getDescription(); + pluginName = pdf.getName(); + log.info("[" + pluginName + "] Is Loading, Version: " + pdf.getVersion()); + + this.JIPCache = new JIPCache(plugin); + this.jSettings = new JSettings(new File(this.getDataFolder(), "config.yml")); + + JListener jListener = new JListener(this); + Bukkit.getPluginManager().registerEvents(jListener, this); + } + + @Override + public void onDisable() { + log.info("[" + pluginName + "] Is Disabling, Version: " + pdf.getVersion()); + JIPCache.saveData(); + } + + public JSettings getjSettings() { + return jSettings; + } + + public HashMap getBetaEVOVerificationResults() { + return betaEVOVerificationResults; + } + + public com.johnymuffin.beta.jdetector.JIPCache getJIPCache() { + return JIPCache; + } + + public void logger(Level level, String message) { + Bukkit.getLogger().log(level, "[" + pluginName + "] " + message); + } +} diff --git a/src/com/johnymuffin/beta/jdetector/JIPCache.java b/src/com/johnymuffin/beta/jdetector/JIPCache.java new file mode 100644 index 0000000..4830ed4 --- /dev/null +++ b/src/com/johnymuffin/beta/jdetector/JIPCache.java @@ -0,0 +1,90 @@ +package com.johnymuffin.beta.jdetector; + +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.logging.Level; + +public class JIPCache { + private JDetector plugin; + private JSONObject ipCacheJSON; + private File cacheFile; + private boolean memoryOnly = false; + + public JIPCache(JDetector plugin) { + this.plugin = plugin; + cacheFile = new File(plugin.getDataFolder() + File.separator + "cache" + File.separator + "ipCache.json"); + boolean isNew = false; + if (!cacheFile.exists()) { + cacheFile.getParentFile().mkdirs(); + try { + FileWriter file = new FileWriter(cacheFile); + plugin.logger(Level.INFO, "Generating ipCache.json file"); + ipCacheJSON = new JSONObject(); + file.write(ipCacheJSON.toJSONString()); + file.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + isNew = true; + } + + try { + plugin.logger(Level.INFO, "Reading ipCache.json file"); + JSONParser parser = new JSONParser(); + ipCacheJSON = (JSONObject) parser.parse(new FileReader(cacheFile)); + } catch (ParseException e) { + plugin.logger(Level.WARNING, "ipCache.json file is corrupt, resetting file: " + e + " : " + e.getMessage()); + ipCacheJSON = new JSONObject(); + } catch (Exception e) { + plugin.logger(Level.WARNING, "ipCache.json file is corrupt, changing to memory only mode."); + memoryOnly = true; + ipCacheJSON = new JSONObject(); + } + saveData(); + + } + + public void saveIPData(String ip, boolean vpn) { + JSONObject ipData = new JSONObject(); + ipData.put("vpn", vpn); + ipData.put("lastChecked", (System.currentTimeMillis()/1000L)); + ipCacheJSON.put(ip, ipData); + this.saveData(); + } + + public boolean isIPSaved(String ip) { + return ipCacheJSON.containsKey(ip); + } + + public boolean isVPN(String ip) { + return Boolean.valueOf(String.valueOf(((JSONObject) ipCacheJSON.get(ip)).get("vpn"))); + } + + public long getLastChecked(String ip) { + return Long.valueOf(String.valueOf(((JSONObject) ipCacheJSON.get(ip)).get("lastChecked"))); + } + + public void saveData() { + saveJsonArray(); + } + + private void saveJsonArray() { + if (memoryOnly) { + return; + } + try (FileWriter file = new FileWriter(cacheFile)) { + plugin.logger(Level.INFO, "Saving ipCache.json"); + file.write(ipCacheJSON.toJSONString()); + file.flush(); + } catch (IOException e) { + plugin.logger(Level.WARNING, "Error saving ipCache.json: " + e + " : " + e.getMessage()); + } + } + +} diff --git a/src/com/johnymuffin/beta/jdetector/JListener.java b/src/com/johnymuffin/beta/jdetector/JListener.java new file mode 100644 index 0000000..7228b07 --- /dev/null +++ b/src/com/johnymuffin/beta/jdetector/JListener.java @@ -0,0 +1,169 @@ +package com.johnymuffin.beta.jdetector; + +import com.johnymuffin.beta.jdetector.utils.BetaEvolutionsUtils; +import com.projectposeidon.johnymuffin.ConnectionPause; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerPreLoginEvent; +import org.bukkit.plugin.Plugin; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.logging.Level; + +public class JListener implements Listener { + private JDetector jDetector; + + private BetaEvolutionsUtils betaEvolutionsUtils; + + public JListener(JDetector jDetector) { + this.jDetector = jDetector; + this.betaEvolutionsUtils = new BetaEvolutionsUtils(false); //Disable debug for now + } + + @EventHandler + public void onPlayerPreLogin(final PlayerPreLoginEvent event) { + String playerName = event.getName(); + String playerIP = event.getAddress().getHostAddress(); + + // Beta Evolutions Check + ConnectionPause betaEVOConnectionPause = event.addConnectionPause(this.jDetector, "BetaEVO"); + Bukkit.getScheduler().scheduleAsyncDelayedTask(this.jDetector, () -> { + final BetaEvolutionsUtils.VerificationResults verificationResult = betaEvolutionsUtils.verifyUser(playerName, playerIP); + Bukkit.getScheduler().scheduleSyncDelayedTask(this.jDetector, () -> { + jDetector.getBetaEVOVerificationResults().put(playerName + "-" + playerIP, verificationResult); + betaEVOConnectionPause.removeConnectionPause(); + }); + }); + + // IPHub Check + + //Check if IP lookup has occurred in the last week + boolean checkIP = true; + + if (jDetector.getJIPCache().isIPSaved(playerIP)) { + if (jDetector.getJIPCache().getLastChecked(playerIP) + 604800 > (System.currentTimeMillis()/1000L)) { + checkIP = false; + } + } + + if(!checkIP) { + jDetector.logger(Level.INFO, "IPHub check skipped for " + playerName + " as IP has been checked in the last week."); + return; + } + + ConnectionPause ipHubConnectionPause = event.addConnectionPause(this.jDetector, "IPHub"); + int timeout = 5000; + String Xkey = this.jDetector.getjSettings().getString("settings.api.key"); + Bukkit.getScheduler().scheduleAsyncDelayedTask(this.jDetector, () -> { + try { + String endpointURL = this.jDetector.getjSettings().getConfigString("settings.api.url"); + endpointURL = endpointURL.replace("{player_ip}", playerIP); + URL myURL = new URL(endpointURL); + HttpURLConnection connection = (HttpURLConnection) myURL.openConnection(); + connection.setConnectTimeout(timeout); + connection.setReadTimeout(timeout); + connection.setRequestMethod("GET"); + connection.setRequestProperty("X-Key", Xkey); + connection.connect(); + + // Check response code + int responseCode = connection.getResponseCode(); + + if (responseCode != 200) { + jDetector.logger(Level.WARNING, "IPHub returned a response code of " + responseCode + " for " + playerIP); + ipHubConnectionPause.removeConnectionPause(); + return; + } + + JSONObject response = null; + + // Read response + InputStream is = connection.getInputStream(); + try { + BufferedReader rd = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8"))); + String jsonText = readAll(rd); + JSONParser parser = new JSONParser(); + response = (JSONObject) parser.parse(jsonText); + } catch (Exception exception) { + jDetector.logger(Level.WARNING, "An exception occurred while reading the response from IPHub for " + playerIP); + exception.printStackTrace(); + ipHubConnectionPause.removeConnectionPause(); + } finally { + is.close(); + } + + JSONObject finalResponse = response; + Bukkit.getScheduler().scheduleSyncDelayedTask(this.jDetector, () -> { + ipHubConnectionPause.removeConnectionPause(); + boolean isLikelyProxy = false; + if(Integer.parseInt(finalResponse.get("block").toString()) >= 1) { + isLikelyProxy = true; + } + + // Save IP information + jDetector.getJIPCache().saveIPData(playerIP, isLikelyProxy); + }); + + + } catch (Exception exception) { + ipHubConnectionPause.removeConnectionPause(); + this.jDetector.logger(Level.WARNING, "Error while checking IPHub: "); + exception.printStackTrace(); + } + }); + + + } + + @EventHandler(priority = Event.Priority.Lowest) + public void onPlayerLogin(final PlayerLoginEvent event) { + //Check if player is actually allowed to join already + if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { + return; + } + String playerIP = event.getAddress().getHostAddress(); + BetaEvolutionsUtils.VerificationResults verificationResult = jDetector.getBetaEVOVerificationResults().get(event.getPlayer().getName() + "-" + playerIP); + if (verificationResult.getSuccessful() >= 1) { + this.jDetector.logger(Level.INFO, "Player " + event.getPlayer().getName() + " has been verified by Beta Evolutions and has bypassed VPN checks."); + return; + } + + //Check if users is utilizing a VPN + if (!this.jDetector.getJIPCache().isIPSaved(playerIP)) { + this.jDetector.logger(Level.WARNING, "Player " + event.getPlayer().getName() + " doesn't have an IP saved in the cache. Likely, we have exceeded our API limit. They will be allowed to join."); + return; + } + + if (this.jDetector.getJIPCache().isVPN(playerIP)) { + this.jDetector.logger(Level.WARNING, "Player " + event.getPlayer().getName() + " has been detected as using a VPN. They will be kicked."); + event.disallow(PlayerLoginEvent.Result.KICK_OTHER, ChatColor.RED + "VPN detected. Try connecting with Beta Evolutions, bit.ly/BetaEVO"); + return; + } + + this.jDetector.logger(Level.INFO, "Player " + event.getPlayer().getName() + " has been verified as not using a VPN."); + + + } + + private static String readAll(Reader rd) throws IOException { + StringBuilder sb = new StringBuilder(); + int cp; + while ((cp = rd.read()) != -1) + sb.append((char) cp); + return sb.toString(); + } + + +} diff --git a/src/com/johnymuffin/beta/jdetector/JSettings.java b/src/com/johnymuffin/beta/jdetector/JSettings.java new file mode 100644 index 0000000..db8a696 --- /dev/null +++ b/src/com/johnymuffin/beta/jdetector/JSettings.java @@ -0,0 +1,81 @@ +package com.johnymuffin.beta.jdetector; + +import org.bukkit.util.config.Configuration; + +import java.io.File; + +public class JSettings extends Configuration { + private boolean isNew = true; + + public JSettings(File file) { + super(file); + this.isNew = !file.exists(); + reload(); + } + + public void reload() { + this.load(); + this.write(); + this.save(); + } + + private void write() { + generateConfigOption("settings.api.url", "https://v2.api.iphub.info/ip/{player_ip}"); + generateConfigOption("settings.api.key", "123pass"); + } + + private void generateConfigOption(String key, Object defaultValue) { + if (this.getProperty(key) == null) { + this.setProperty(key, defaultValue); + } + final Object value = this.getProperty(key); + this.removeProperty(key); + this.setProperty(key, value); + } + + public Object getConfigOption(String key) { + return this.getProperty(key); + } + //Getters Start + + public String getConfigString(String key) { + return String.valueOf(getConfigOption(key)); + } + + public Integer getConfigInteger(String key) { + return Integer.valueOf(getConfigString(key)); + } + + public Long getConfigLong(String key) { + return Long.valueOf(getConfigString(key)); + } + + public Double getConfigDouble(String key) { + return Double.valueOf(getConfigString(key)); + } + + public Boolean getConfigBoolean(String key) { + return Boolean.valueOf(getConfigString(key)); + } + + + //Getters End + + private boolean convertToNewAddress(String newKey, String oldKey) { + if (this.getString(newKey) != null) { + return false; + } + if (this.getString(oldKey) == null) { + return false; + } + System.out.println("Converting Config: " + oldKey + " to " + newKey); + Object value = this.getProperty(oldKey); + this.setProperty(newKey, value); + this.removeProperty(oldKey); + return true; + } + + public boolean isNew() { + return isNew; + } +} diff --git a/src/com/johnymuffin/beta/jdetector/utils/BetaEvolutionsUtils.java b/src/com/johnymuffin/beta/jdetector/utils/BetaEvolutionsUtils.java new file mode 100644 index 0000000..96b591c --- /dev/null +++ b/src/com/johnymuffin/beta/jdetector/utils/BetaEvolutionsUtils.java @@ -0,0 +1,463 @@ +package com.johnymuffin.beta.jdetector.utils; + +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; + +/* + * A utility class (wrapper maybe?) for communicating with Beta Evolutions nodes + * + * @author RhysB + * @version 1.0.0 + * @website https://evolutions.johnymuffin.com/ + * + * This class has a license :) + * ------------------------------------------------------------------------------ + * MIT License + * Copyright (c) 2020 Rhys B + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * ------------------------------------------------------------------------------ + * + */ + +public class BetaEvolutionsUtils { + private boolean debug; + + + public BetaEvolutionsUtils() { + debug = false; + } + + public BetaEvolutionsUtils(boolean debug) { + this.debug = debug; + } + + //Core Methods - Start + + /** + * Attempt to authenticate the user with Evolution nodes + * !!!!This class is blocking + * + * @return VerificationResults class which contains successful/failed nodes + */ + public VerificationResults authenticateUser(String username, String sessionID) { + //Fetch IP address for V2 and above support + String ip = getExternalIP(); + if (ip == null) { + log("Can't authenticate with any nodes, can't fetch external IP address. Your internet is probably offline!"); + return new VerificationResults(0, 0, beServers.size()); + } + VerificationResults verificationResults = new VerificationResults(); + //Iterate through all nodes while verifying + for (String node : beServers.keySet()) { + Boolean result = authenticateWithBetaEvolutions(username, node, beServers.get(node), sessionID, ip); + if (result == null) { + verificationResults.setErrored(verificationResults.getErrored() + 1); + } else if (result == true) { + verificationResults.setSuccessful(verificationResults.getSuccessful() + 1); + } else if (result == false) { + verificationResults.setFailed(verificationResults.getFailed() + 1); + } + } + return verificationResults; + + } + + /** + * Check if a user is authenticated with Evolution nodes + * !!!!This class is blocking + * + * @return VerificationResults class which contains successful/failed nodes. Failed nodes mean an error occurred or the user wasn't verified. Successful means the user was verified/authenticated by the node. + */ + public VerificationResults verifyUser(String username, String userIP) { + VerificationResults verificationResults = new VerificationResults(); + //Iterate through all nodes while verifying + for (String node : beServers.keySet()) { + Boolean result = verifyUserWithNode(username, userIP, node, beServers.get(node)); + if (result == null) { + verificationResults.setErrored(verificationResults.getErrored() + 1); + } else if (result == true) { + verificationResults.setSuccessful(verificationResults.getSuccessful() + 1); + } else if (result == false) { + verificationResults.setFailed(verificationResults.getFailed() + 1); + } + //If the user is verified with the node, break out of the loop. This is just for this plugin to speed stuff up. + if(verificationResults.successful >= 1) { + break; + } + + } + return verificationResults; + } + + + //Core Methods - End + + //Server Methods - Start + + private Boolean verifyUserWithNode(String username, String userIP, String node, BEVersion beVersion) { + //Return Types + //True Successfully Authenticated + //False Failed, probably didn't auth with that node + //Null, an error occurred, probably internet outage + if (beVersion == BEVersion.V1) { + //Stage 1 - Contact node to confirm identification + String stage1URL = node + "/serverAuth.php?method=1&username=" + encodeString(username) + "&userip=" + encodeString(userIP); + JSONObject stage1Object = getJSONFromURL(stage1URL); + if (stage1Object == null) { + log("Authentication with node: " + node + " has failed to respond when queried."); + return null; + } + if (!verifyJSONArguments(stage1Object, "result", "verified")) { + log("Malformed response from: " + node + " using version " + beVersion); + return null; + } + return Boolean.valueOf(String.valueOf(stage1Object.get("verified"))); + } else if (beVersion == BEVersion.V2_PLAINTEXT) { + //Stage 1 - Contact node to confirm identification + String stage1URL = node + "/server/getVerification?username=" + encodeString(username) + "&userip=" + encodeString(userIP); + JSONObject stage1Object = getJSONFromURL(stage1URL); + if (stage1Object == null) { + log("Authentication with node: " + node + " has failed to respond when queried."); + return null; + } + if (!verifyJSONArguments(stage1Object, "verified", "error")) { + log("Malformed response from: " + node + " using version " + beVersion); + return null; + } + return Boolean.valueOf(String.valueOf(stage1Object.get("verified"))); + } + + return null; + } + + //Server Methods - End + + + //Client Methods - Start + private Boolean authenticateWithMojang(String username, String sessionID, String serverID) { + try { + String authURL = "http://session.minecraft.net/game/joinserver.jsp?user=" + encodeString(username) + "&sessionId=" + encodeString(sessionID) + "&serverId=" + encodeString(serverID); + URL url = new URL(authURL); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(url.openStream())); + String response = bufferedReader.readLine(); + bufferedReader.close(); + if (response.equalsIgnoreCase("ok")) { + return true; + } else { + return false; + } + } catch (Exception e) { + if (debug) { + log("An error occurred contacting Mojang."); + e.printStackTrace(); + } + } + return null; + } + + private Boolean authenticateWithBetaEvolutions(String username, String node, BEVersion beVersion, String sessionToken, String ip) { + //Return Types + //True Successfully Authenticated + //False Failed, probably a cracked user + //Null, an error occurred, probably internet outage + if (beVersion == BEVersion.V1) { + //State 1 - Contact the node with username and method type + String stage1URL = node + "/userAuth.php?method=1&username=" + encodeString(username); + JSONObject stage1Object = getJSONFromURL(stage1URL); + if (stage1Object == null) { + log("Authentication with node: " + node + " has failed as JSON can't be fetched."); + return null; + } + if (!verifyJSONArguments(stage1Object, "result", "username", "userip", "serverId")) { + log("Malformed response from: " + node + " using version " + beVersion); + return null; + } + String serverID = String.valueOf(stage1Object.get("serverId")); + //Stage 2 - Contact Mojang to authenticate + Boolean mojangAuthentication = authenticateWithMojang(username, sessionToken, serverID); + if (mojangAuthentication == null) { + log("Authentication with node: " + node + " has failed due to auth failure with Mojang."); + return null; + } else if (mojangAuthentication == false) { + log("Authentication with node: " + node + " has failed. Token is probably incorrect, or user is cracked!"); + return false; + } + //Stage 3 - Contact node to confirm auth + String stage3URL = node + "/userAuth.php?method=2&username=" + encodeString(username) + "&serverId=" + encodeString(serverID); + JSONObject stage3Object = getJSONFromURL(stage3URL); + if (stage3Object == null) { + log("Authentication with node: " + node + " has failed as JSON can't be fetched."); + return null; + } + if (!verifyJSONArguments(stage3Object, "result")) { + log("Malformed response from: " + node + " using version " + beVersion); + return null; + } + return Boolean.valueOf(String.valueOf(stage3Object.get("result"))); + + + } else if (beVersion == BEVersion.V2_PLAINTEXT) { + //State 1 - Contact the node with username and method type + String stage1URL = node + "/user/getServerID?username=" + encodeString(username) + "&userip=" + ip; + JSONObject stage1Object = getJSONFromURL(stage1URL); + if (stage1Object == null) { + log("Authentication with node: " + node + " has failed as JSON can't be fetched."); + return null; + } + if (!verifyJSONArguments(stage1Object, "userIP", "error", "serverID", "username")) { + log("Malformed response from: " + node + " using version " + beVersion); + return null; + } + String serverID = String.valueOf(stage1Object.get("serverID")); + //Stage 2 - Contact Mojang to authenticate + Boolean mojangAuthentication = authenticateWithMojang(username, sessionToken, serverID); + if (mojangAuthentication == null) { + log("Authentication with node: " + node + " has failed due to auth failure with Mojang."); + return null; + } else if (mojangAuthentication == false) { + log("Authentication with node: " + node + " has failed. Token is probably incorrect, or user is cracked!"); + return false; + } + //Stage 3 - Contact node to confirm auth + String stage3URL = node + "/user/successfulAuth?username=" + encodeString(username) + "&serverid=" + encodeString(serverID) + "&userip=" + encodeString(ip); + JSONObject stage3Object = getJSONFromURL(stage3URL); + if (stage3Object == null) { + log("Authentication with node: " + node + " has failed as JSON can't be fetched."); + return null; + } + if (!verifyJSONArguments(stage3Object, "result")) { + log("Malformed response from: " + node + " using version " + beVersion); + return null; + } + return Boolean.valueOf(String.valueOf(stage3Object.get("result"))); + } + return null; + } + + private String getExternalIP() { + String ip = getIPFromAmazon(); + if (ip == null) { + ip = getIPFromWhatIsMyIpAddress(); + } + return ip; + } + + + private String getIPFromAmazon() { + try { + URL myIP = new URL("http://checkip.amazonaws.com"); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(myIP.openStream())); + return bufferedReader.readLine(); + + } catch (Exception e) { + log("Failed to get IP from Amazon, your internet is probably down."); + if (debug) { + e.printStackTrace(); + } + } + return null; + } + + private String getIPFromWhatIsMyIpAddress() { + try { + URL myIP = new URL("https://ipv4bot.whatismyipaddress.com/"); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(myIP.openStream())); + return bufferedReader.readLine(); + + } catch (Exception e) { + log("Failed to get IP from WhatIsMyIpAddress, your internet is probably down."); + if (debug) { + e.printStackTrace(); + } + } + return null; + } + + + //Client Methods - End + + /*Node Storage - Start + + */ + private static HashMap beServers = new HashMap<>(); + + static { + //Some nodes may support multiple types. Ideally in the future, this class will contact nodes asking for their protocol versions, however, V2 should remain online. +// beServers.put("https://auth.johnymuffin.com", BEVersion.V1); + beServers.put("https://auth1.evolutions.johnymuffin.com", BEVersion.V2_PLAINTEXT); + beServers.put("https://auth2.evolutions.johnymuffin.com", BEVersion.V2_PLAINTEXT); +// beServers.put("https://auth3.evolutions.johnymuffin.com", BEVersion.V2_PLAINTEXT); //This node is currently offline + } + + public enum BEVersion { + V1, + V2_PLAINTEXT, + } + + //Node Storage - Start + + //Utils - Start + + //Method readJsonFromUrl and readAll licensed under CC BY-SA 2.5 (https://stackoverflow.com/help/licensing) + //Credit: https://stackoverflow.com/a/4308662 + + private static JSONObject readJsonFromUrl(String url) throws IOException, ParseException { +// InputStream is = (new URL(url)).openStream(); +// +// try { +// BufferedReader rd = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8"))); +// String jsonText = readAll(rd); +// JSONObject json = new JSONObject(jsonText); +// return json; +// } finally { +// is.close(); +// } + return readJsonFromUrlWithTimeout(url, 5000); + } + + //Read JSON from URL with timeout + private static JSONObject readJsonFromUrlWithTimeout(String url, int timeout) throws IOException, ParseException { + URL myURL = new URL(url); + HttpURLConnection connection = (HttpURLConnection) myURL.openConnection(); + connection.setConnectTimeout(timeout); + connection.setReadTimeout(timeout); + connection.setRequestMethod("GET"); + connection.connect(); + InputStream is = connection.getInputStream(); + try { + BufferedReader rd = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8"))); + String jsonText = readAll(rd); + + JSONParser parser = new JSONParser(); + + JSONObject json = (JSONObject) parser.parse(jsonText); + return json; + } finally { + is.close(); + } + } + + private static String readAll(Reader rd) throws IOException { + StringBuilder sb = new StringBuilder(); + int cp; + while ((cp = rd.read()) != -1) + sb.append((char) cp); + return sb.toString(); + } + + + private JSONObject getJSONFromURL(String url) { + try { + JSONObject jsonObject = readJsonFromUrl(url); + return jsonObject; + } catch (Exception e) { + if (debug) { + log("An error occurred fetching JSON from: " + url); + e.printStackTrace(); + } + } + return null; + } + + + private void log(String info) { + if (debug) { + System.out.println("[Beta Evolutions] " + info); + } + } + + private String encodeString(String string) { + try { + return URLEncoder.encode(string, StandardCharsets.UTF_8.toString()); + } catch (Exception e) { + log("An error occurred encoding a string, this really shouldn't happen on modern JVMs."); + e.printStackTrace(); + } + return null; + } + + + private boolean verifyJSONArguments(JSONObject jsonObject, String... arguments) { + for (String s : arguments) { + if (!jsonObject.containsKey(s)) return false; + } + return true; + } + + + //Utils - End + + + public class VerificationResults { + private int successful = 0; + private int failed = 0; + private int errored = 0; + + public VerificationResults() { + } + + public VerificationResults(int successful, int failed, int errored) { + this.successful = successful; + this.failed = failed; + this.errored = errored; + } + + + public int getSuccessful() { + return successful; + } + + public void setSuccessful(int successful) { + this.successful = successful; + } + + public int getFailed() { + return failed; + } + + public void setFailed(int failed) { + this.failed = failed; + } + + public int getErrored() { + return errored; + } + + public void setErrored(int errored) { + this.errored = errored; + } + + public int getTotal() { + return (errored + successful + failed); + } + + } + + +} diff --git a/src/plugin.yml b/src/plugin.yml new file mode 100644 index 0000000..817bc3a --- /dev/null +++ b/src/plugin.yml @@ -0,0 +1,6 @@ +name: JDetector +description: A VPN detector plugin designed primary for RetroMC +main: com.johnymuffin.beta.jdetector.JDetector +version: 1.0.0 +authors: + - JohnyMuffin