diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java index 1e84e13d9..ca41fbd72 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java @@ -51,6 +51,7 @@ import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.platform.standalone.command.GeyserCommandManager; import org.geysermc.geyser.platform.standalone.gui.GeyserStandaloneGUI; +import org.geysermc.geyser.util.LoopbackUtil; import java.io.File; import java.io.IOException; @@ -187,7 +188,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { geyserLogger = new GeyserStandaloneLogger(); - LoopbackUtil.checkLoopback(geyserLogger); + LoopbackUtil.checkAndApplyLoopback(geyserLogger); try { File configFile = FileUtils.fileOrCopiedFromResource(new File(configFilename), "config.yml", diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 0cea9fbac..4322dde59 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -73,8 +73,6 @@ import org.geysermc.geyser.translator.inventory.item.ItemTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.*; -import javax.naming.directory.Attribute; -import javax.naming.directory.InitialDirContext; import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -226,23 +224,12 @@ public class GeyserImpl implements GeyserApi { String remoteAddress = config.getRemote().getAddress(); // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry. if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { - int remotePort; - try { - // Searches for a server address and a port from a SRV record of the specified host name - InitialDirContext ctx = new InitialDirContext(); - Attribute attr = ctx.getAttributes("dns:///_minecraft._tcp." + remoteAddress, new String[]{"SRV"}).get("SRV"); - // size > 0 = SRV entry found - if (attr != null && attr.size() > 0) { - String[] record = ((String) attr.get(0)).split(" "); - // Overwrites the existing address and port with that from the SRV record. - config.getRemote().setAddress(remoteAddress = record[3]); - config.getRemote().setPort(remotePort = Integer.parseInt(record[2])); - logger.debug("Found SRV record \"" + remoteAddress + ":" + remotePort + "\""); - } - } catch (Exception | NoClassDefFoundError ex) { // Check for a NoClassDefFoundError to prevent Android crashes - logger.debug("Exception while trying to find an SRV record for the remote host."); - if (config.isDebugMode()) - ex.printStackTrace(); // Otherwise we can get a stack trace for any domain that doesn't have an SRV record + String[] record = WebUtils.findSrvRecord(this, remoteAddress); + if (record != null) { + int remotePort = Integer.parseInt(record[2]); + config.getRemote().setAddress(remoteAddress = record[3]); + config.getRemote().setPort(remotePort); + logger.debug("Found SRV record \"" + remoteAddress + ":" + remotePort + "\""); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandManager.java b/core/src/main/java/org/geysermc/geyser/command/CommandManager.java index 60af8c4e5..38a86fdd0 100644 --- a/core/src/main/java/org/geysermc/geyser/command/CommandManager.java +++ b/core/src/main/java/org/geysermc/geyser/command/CommandManager.java @@ -55,6 +55,7 @@ public abstract class CommandManager { registerCommand(new StatisticsCommand(geyser, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics")); registerCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); registerCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips")); + registerCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest")); if (GeyserImpl.getInstance().getPlatformType() == PlatformType.STANDALONE) { registerCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java new file mode 100644 index 000000000..576d17128 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * 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. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.command.defaults; + +import com.fasterxml.jackson.databind.JsonNode; +import org.geysermc.common.PlatformType; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.command.CommandSender; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.LoopbackUtil; +import org.geysermc.geyser.util.WebUtils; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; + +public class ConnectionTestCommand extends GeyserCommand { + private final GeyserImpl geyser; + + public ConnectionTestCommand(GeyserImpl geyser, String name, String description, String permission) { + super(name, description, permission); + this.geyser = geyser; + } + + @Override + public void execute(@Nullable GeyserSession session, CommandSender sender, String[] args) { + // Only allow the console to create dumps on Geyser Standalone + if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { + sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.getLocale())); + return; + } + + if (args.length == 0) { + sender.sendMessage("Provide the Bedrock server IP you are trying to connect with. Example: `test.geysermc.org:19132`"); + return; + } + + // Still allow people to not supply a port and fallback to 19132 + String[] fullAddress = args[0].split(":", 2); + int port; + if (fullAddress.length == 2) { + port = Integer.parseInt(fullAddress[1]); + } else { + port = 19132; + } + + // Issue: do the ports not line up? + if (port != geyser.getConfig().getBedrock().getPort()) { + sender.sendMessage("The port you supplied (" + port + ") does not match the port supplied in Geyser's configuration (" + + geyser.getConfig().getBedrock().getPort() + "). You can change it under `bedrock` `port`."); + } + + // Issue: is the `bedrock` `address` in the config different? + if (!geyser.getConfig().getBedrock().getAddress().equals("0.0.0.0")) { + sender.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional."); + } + + // Issue: did someone turn on enable-proxy-protocol and they didn't mean it? + if (geyser.getConfig().getBedrock().isEnableProxyProtocol()) { + sender.sendMessage("You have the `enable-proxy-protocol` setting enabled. " + + "Unless you're deliberately using additional software that REQUIRES this setting, you may not need it enabled."); + } + + CompletableFuture.runAsync(() -> { + try { + // Issue: SRV record? + String ip = fullAddress[0]; + String[] record = WebUtils.findSrvRecord(geyser, ip); + if (record != null && !ip.equals(record[3]) && !record[2].equals(String.valueOf(port))) { + sender.sendMessage("Bedrock Edition does not support SRV records. Try connecting to your server using the address " + record[3] + " and the port " + record[2] + + ". If that fails, re-run this command with that address and port."); + return; + } + + // Issue: does Loopback need applying? + if (LoopbackUtil.needsLoopback(GeyserImpl.getInstance().getLogger())) { + sender.sendMessage("Loopback is not applied on this computer! You will have issues connecting from the same computer. " + + "See here for steps on how to resolve: " + "https://wiki.geysermc.org/geyser/fixing-unable-to-connect-to-world/#using-geyser-on-the-same-computer"); + } + + // mcsrvstatus will likely be replaced in the future with our own service where we can also test + // around the OVH workaround without worrying about caching + JsonNode output = WebUtils.getJson("https://api.mcsrvstat.us/bedrock/2/" + args[0]); + + long cacheTime = output.get("debug").get("cachetime").asLong(); + String when; + if (cacheTime == 0) { + when = "now"; + } else { + when = ((System.currentTimeMillis() / 1000L) - cacheTime) + " seconds ago"; + } + + if (output.get("online").asBoolean()) { + sender.sendMessage("Your server is likely online as of " + when + "!"); + sendLinks(sender); + return; + } + + sender.sendMessage("Your server is likely unreachable from outside the network as of " + when + "."); + sendLinks(sender); + } catch (Exception e) { + sender.sendMessage("Error while trying to check your connection!"); + geyser.getLogger().error("Error while trying to check your connection!", e); + } + }); + } + + private void sendLinks(CommandSender sender) { + sender.sendMessage("If you still have issues, check to see if your hosting provider has a specific setup: " + + "https://wiki.geysermc.org/geyser/supported-hosting-providers/" + ", see this page: " + + "https://wiki.geysermc.org/geyser/fixing-unable-to-connect-to-world/" + ", or contact us on our Discord: " + "https://discord.gg/geysermc"); + } + + @Override + public boolean isSuggestedOpOnly() { + return true; + } +} diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/LoopbackUtil.java b/core/src/main/java/org/geysermc/geyser/util/LoopbackUtil.java similarity index 52% rename from bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/LoopbackUtil.java rename to core/src/main/java/org/geysermc/geyser/util/LoopbackUtil.java index 6679c8950..b543e4a48 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/LoopbackUtil.java +++ b/core/src/main/java/org/geysermc/geyser/util/LoopbackUtil.java @@ -23,8 +23,9 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.platform.standalone; +package org.geysermc.geyser.util; +import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; @@ -32,32 +33,47 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; -public class LoopbackUtil { - private static final String checkExemption = "powershell -Command \"CheckNetIsolation LoopbackExempt -s\""; // Java's Exec feature runs as CMD, NetIsolation is only accessible from PowerShell. - private static final String loopbackCommand = "powershell -Command \"CheckNetIsolation LoopbackExempt -a -n='Microsoft.MinecraftUWP_8wekyb3d8bbwe'\""; +public final class LoopbackUtil { + private static final String checkExemption = "CheckNetIsolation LoopbackExempt -s"; + private static final String loopbackCommand = "CheckNetIsolation LoopbackExempt -a -n='Microsoft.MinecraftUWP_8wekyb3d8bbwe'"; + /** + * This string needs to be checked in the event Minecraft is not installed - no Minecraft string will be present in the checkExemption command. + */ + private static final String minecraftApplication = "S-1-15-2-1958404141-86561845-1752920682-3514627264-368642714-62675701-733520436"; private static final String startScript = "powershell -Command \"Start-Process 'cmd' -ArgumentList /c,%temp%/loopback_minecraft.bat -Verb runAs\""; - public static void checkLoopback(GeyserStandaloneLogger geyserLogger) { - if (System.getProperty("os.name").equalsIgnoreCase("Windows 10")) { + /** + * @return true if loopback is not addressed properly. + */ + public static boolean needsLoopback(GeyserLogger logger) { + String os = System.getProperty("os.name"); + if (os.equalsIgnoreCase("Windows 10") || os.equalsIgnoreCase("Windows 11")) { try { Process process = Runtime.getRuntime().exec(checkExemption); + process.waitFor(); InputStream is = process.getInputStream(); + StringBuilder sb = new StringBuilder(); - - while (process.isAlive()) { - if (is.available() != 0) { - sb.append((char) is.read()); - } + while (is.available() != 0) { + sb.append((char) is.read()); } - String result = sb.toString(); + return !sb.toString().contains(minecraftApplication); + } catch (Exception e) { + logger.error("Couldn't detect if loopback has been added on Windows!", e); + return true; + } + } + return false; + } - if (!result.contains("minecraftuwp")) { - Files.write(Paths.get(System.getenv("temp") + "/loopback_minecraft.bat"), loopbackCommand.getBytes()); - Runtime.getRuntime().exec(startScript); + public static void checkAndApplyLoopback(GeyserLogger geyserLogger) { + if (needsLoopback(geyserLogger)) { + try { + Files.write(Paths.get(System.getenv("temp") + "/loopback_minecraft.bat"), loopbackCommand.getBytes()); + Runtime.getRuntime().exec(startScript); - geyserLogger.info(ChatColor.AQUA + GeyserLocale.getLocaleStringLog("geyser.bootstrap.loopback.added")); - } + geyserLogger.info(ChatColor.AQUA + GeyserLocale.getLocaleStringLog("geyser.bootstrap.loopback.added")); } catch (Exception e) { e.printStackTrace(); @@ -66,4 +82,6 @@ public class LoopbackUtil { } } + private LoopbackUtil() { + } } diff --git a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java index fe479363f..f9574f08b 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -28,6 +28,9 @@ package org.geysermc.geyser.util; import com.fasterxml.jackson.databind.JsonNode; import org.geysermc.geyser.GeyserImpl; +import javax.annotation.Nullable; +import javax.naming.directory.Attribute; +import javax.naming.directory.InitialDirContext; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; @@ -170,4 +173,23 @@ public class WebUtils { return connectionToString(con); } + + @Nullable + public static String[] findSrvRecord(GeyserImpl geyser, String remoteAddress) { + try { + // Searches for a server address and a port from a SRV record of the specified host name + InitialDirContext ctx = new InitialDirContext(); + Attribute attr = ctx.getAttributes("dns:///_minecraft._tcp." + remoteAddress, new String[]{"SRV"}).get("SRV"); + // size > 0 = SRV entry found + if (attr != null && attr.size() > 0) { + return ((String) attr.get(0)).split(" "); + } + } catch (Exception | NoClassDefFoundError ex) { // Check for a NoClassDefFoundError to prevent Android crashes + if (geyser.getConfig().isDebugMode()) { + geyser.getLogger().debug("Exception while trying to find an SRV record for the remote host."); + ex.printStackTrace(); // Otherwise we can get a stack trace for any domain that doesn't have an SRV record + } + } + return null; + } }