diff --git a/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitConfiguration.java b/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitConfiguration.java index 9ea2da2b..df98b408 100644 --- a/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitConfiguration.java +++ b/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitConfiguration.java @@ -92,8 +92,23 @@ public class GeyserBukkitConfiguration implements GeyserConfiguration { } @Override - public boolean isPingPassthrough() { - return config.getBoolean("ping-passthrough", false); + public boolean isPassthroughMotd() { + return config.getBoolean("passthrough-motd", false); + } + + @Override + public boolean isPassthroughPlayerCounts() { + return config.getBoolean("passthrough-player-counts", false); + } + + @Override + public boolean isLegacyPingPassthrough() { + return config.getBoolean("legacy-ping-passthrough", false); + } + + @Override + public int getPingPassthroughInterval() { + return config.getInt("ping-passthrough-interval", 3); } @Override diff --git a/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitPingPassthrough.java b/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitPingPassthrough.java new file mode 100644 index 00000000..812467be --- /dev/null +++ b/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitPingPassthrough.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019-2020 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.platform.bukkit; + +import lombok.AllArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.server.ServerListPingEvent; +import org.bukkit.util.CachedServerIcon; +import org.geysermc.common.ping.GeyserPingInfo; +import org.geysermc.connector.ping.IGeyserPingPassthrough; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.Iterator; + +@AllArgsConstructor +public class GeyserBukkitPingPassthrough implements IGeyserPingPassthrough { + + private final GeyserBukkitLogger logger; + + @Override + public GeyserPingInfo getPingInformation() { + try { + ServerListPingEvent event = new GeyserPingEvent(InetAddress.getLocalHost(), Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers()); + Bukkit.getPluginManager().callEvent(event); + GeyserPingInfo geyserPingInfo = new GeyserPingInfo(event.getMotd(), event.getNumPlayers(), event.getMaxPlayers()); + Bukkit.getOnlinePlayers().forEach(player -> { + geyserPingInfo.addPlayer(player.getName()); + }); + return geyserPingInfo; + } catch (Exception e) { + logger.debug("Error while getting Bukkit ping passthrough: " + e.toString()); + return new GeyserPingInfo(null, 0, 0); + } + } + + // These methods are unimplemented on spigot api by default so we add stubs so plugins don't complain + private static class GeyserPingEvent extends ServerListPingEvent { + + public GeyserPingEvent(InetAddress address, String motd, int numPlayers, int maxPlayers) { + super(address, motd, numPlayers, maxPlayers); + } + + @Override + public void setServerIcon(CachedServerIcon icon) throws IllegalArgumentException, UnsupportedOperationException { + } + + @Override + public Iterator iterator() throws UnsupportedOperationException { + return Collections.EMPTY_LIST.iterator(); + } + } + +} diff --git a/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitPlugin.java b/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitPlugin.java index ad1f6194..5f0e967a 100644 --- a/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitPlugin.java +++ b/bootstrap/bukkit/src/main/java/org/geysermc/platform/bukkit/GeyserBukkitPlugin.java @@ -33,6 +33,8 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; import org.geysermc.connector.command.CommandManager; import org.geysermc.connector.network.translators.world.WorldManager; +import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.platform.bukkit.command.GeyserBukkitCommandExecutor; import org.geysermc.platform.bukkit.command.GeyserBukkitCommandManager; import org.geysermc.platform.bukkit.world.GeyserBukkitBlockPlaceListener; @@ -46,6 +48,7 @@ public class GeyserBukkitPlugin extends JavaPlugin implements GeyserBootstrap { private GeyserBukkitCommandManager geyserCommandManager; private GeyserBukkitConfiguration geyserConfig; private GeyserBukkitLogger geyserLogger; + private IGeyserPingPassthrough geyserBukkitPingPassthrough; private GeyserBukkitBlockPlaceListener blockPlaceListener; private GeyserBukkitWorldManager geyserWorldManager; @@ -77,6 +80,12 @@ public class GeyserBukkitPlugin extends JavaPlugin implements GeyserBootstrap { this.connector = GeyserConnector.start(PlatformType.BUKKIT, this); + if (geyserConfig.isLegacyPingPassthrough()) { + this.geyserBukkitPingPassthrough = GeyserLegacyPingPassthrough.init(connector); + } else { + this.geyserBukkitPingPassthrough = new GeyserBukkitPingPassthrough(geyserLogger); + } + this.geyserCommandManager = new GeyserBukkitCommandManager(this, connector); boolean isViaVersion = false; @@ -122,6 +131,11 @@ public class GeyserBukkitPlugin extends JavaPlugin implements GeyserBootstrap { return this.geyserCommandManager; } + @Override + public IGeyserPingPassthrough getGeyserPingPassthrough() { + return geyserBukkitPingPassthrough; + } + @Override public WorldManager getWorldManager() { return this.geyserWorldManager; diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index b9f06916..dd66db32 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -20,7 +20,7 @@ net.md-5 bungeecord-api - 1.14-SNAPSHOT + 1.15-SNAPSHOT provided @@ -80,4 +80,4 @@ - \ No newline at end of file + diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java index cb9d1fbf..d983aec1 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java @@ -91,8 +91,23 @@ public class GeyserBungeeConfiguration implements GeyserConfiguration { } @Override - public boolean isPingPassthrough() { - return config.getBoolean("ping-passthrough", false); + public boolean isPassthroughMotd() { + return config.getBoolean("passthrough-motd", false); + } + + @Override + public boolean isPassthroughPlayerCounts() { + return config.getBoolean("passthrough-player-counts", false); + } + + @Override + public boolean isLegacyPingPassthrough() { + return config.getBoolean("legacy-ping-passthrough", false); + } + + @Override + public int getPingPassthroughInterval() { + return config.getInt("ping-passthrough-interval", 3); } @Override diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePingPassthrough.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePingPassthrough.java new file mode 100644 index 00000000..c7f8f276 --- /dev/null +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePingPassthrough.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2019-2020 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.platform.bungeecord; + +import lombok.AllArgsConstructor; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.ServerPing; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.connection.PendingConnection; +import net.md_5.bungee.api.event.ProxyPingEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.protocol.ProtocolConstants; +import org.geysermc.common.ping.GeyserPingInfo; +import org.geysermc.connector.ping.IGeyserPingPassthrough; + +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@AllArgsConstructor +public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, Listener { + + private static final GeyserPendingConnection PENDING_CONNECTION = new GeyserPendingConnection(); + + private final ProxyServer proxyServer; + + @Override + public GeyserPingInfo getPingInformation() { + CompletableFuture future = new CompletableFuture<>(); + proxyServer.getPluginManager().callEvent(new ProxyPingEvent(PENDING_CONNECTION, getPingInfo(), (event, throwable) -> { + if (throwable != null) future.completeExceptionally(throwable); + else future.complete(event); + })); + ProxyPingEvent event = future.join(); + GeyserPingInfo geyserPingInfo = new GeyserPingInfo( + event.getResponse().getDescription(), + event.getResponse().getPlayers().getOnline(), + event.getResponse().getPlayers().getMax() + ); + if (event.getResponse().getPlayers().getSample() != null) { + Arrays.stream(event.getResponse().getPlayers().getSample()).forEach(proxiedPlayer -> { + geyserPingInfo.addPlayer(proxiedPlayer.getName()); + }); + } + return geyserPingInfo; + } + + // This is static so pending connection can use it + private static ListenerInfo getDefaultListener() { + return ProxyServer.getInstance().getConfig().getListeners().iterator().next(); + } + + private ServerPing getPingInfo() { + return new ServerPing( + new ServerPing.Protocol(proxyServer.getName() + " " + proxyServer.getGameVersion(), ProtocolConstants.SUPPORTED_VERSION_IDS.get(ProtocolConstants.SUPPORTED_VERSION_IDS.size() - 1)), + new ServerPing.Players(getDefaultListener().getMaxPlayers(), proxyServer.getOnlineCount(), null), + getDefaultListener().getMotd(), proxyServer.getConfig().getFaviconObject() + ); + } + + private static class GeyserPendingConnection implements PendingConnection { + + private static final UUID FAKE_UUID = UUID.nameUUIDFromBytes("geyser!internal".getBytes()); + private static final InetSocketAddress FAKE_REMOTE = new InetSocketAddress(Inet4Address.getLoopbackAddress(), 69); + + @Override + public String getName() { + throw new UnsupportedOperationException(); + } + + @Override + public int getVersion() { + return ProtocolConstants.SUPPORTED_VERSION_IDS.get(ProtocolConstants.SUPPORTED_VERSION_IDS.size() - 1); + } + + @Override + public InetSocketAddress getVirtualHost() { + return null; + } + + @Override + public ListenerInfo getListener() { + return getDefaultListener(); + } + + @Override + public String getUUID() { + return FAKE_UUID.toString(); + } + + @Override + public UUID getUniqueId() { + return FAKE_UUID; + } + + @Override + public void setUniqueId(UUID uuid) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOnlineMode() { + return true; + } + + @Override + public void setOnlineMode(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLegacy() { + return false; + } + + @Override + public InetSocketAddress getAddress() { + return FAKE_REMOTE; + } + + @Override + public SocketAddress getSocketAddress() { + return getAddress(); + } + + @Override + public void disconnect(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public void disconnect(BaseComponent... baseComponents) { + throw new UnsupportedOperationException(); + } + + @Override + public void disconnect(BaseComponent baseComponent) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isConnected() { + return false; + } + + @Override + public Unsafe unsafe() { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java index 270fbd1c..525b9b6d 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java @@ -35,6 +35,8 @@ import org.geysermc.connector.GeyserConfiguration; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; import org.geysermc.connector.command.CommandManager; +import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.platform.bungeecord.command.GeyserBungeeCommandExecutor; import org.geysermc.platform.bungeecord.command.GeyserBungeeCommandManager; @@ -51,6 +53,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { private GeyserBungeeCommandManager geyserCommandManager; private GeyserBungeeConfiguration geyserConfig; private GeyserBungeeLogger geyserLogger; + private IGeyserPingPassthrough geyserBungeePingPassthrough; private GeyserConnector connector; @@ -125,6 +128,12 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { this.geyserCommandManager = new GeyserBungeeCommandManager(connector); + if (geyserConfig.isLegacyPingPassthrough()) { + this.geyserBungeePingPassthrough = GeyserLegacyPingPassthrough.init(connector); + } else { + this.geyserBungeePingPassthrough = new GeyserBungeePingPassthrough(getProxy()); + } + this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor(connector)); } @@ -147,4 +156,9 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { public CommandManager getGeyserCommandManager() { return this.geyserCommandManager; } + + @Override + public IGeyserPingPassthrough getGeyserPingPassthrough() { + return geyserBungeePingPassthrough; + } } diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java index c84f2dfc..fc148470 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java @@ -85,8 +85,23 @@ public class GeyserSpongeConfiguration implements GeyserConfiguration { } @Override - public boolean isPingPassthrough() { - return node.getNode("ping-passthrough").getBoolean(false); + public boolean isPassthroughMotd() { + return node.getNode("passthrough-motd").getBoolean(false); + } + + @Override + public boolean isPassthroughPlayerCounts() { + return node.getNode("passthrough-player-counts").getBoolean(false); + } + + @Override + public boolean isLegacyPingPassthrough() { + return node.getNode("legacy-ping-passthrough").getBoolean(false); + } + + @Override + public int getPingPassthroughInterval() { + return node.getNode("ping-passthrough-interval").getInt(3); } @Override diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePingPassthrough.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePingPassthrough.java new file mode 100644 index 00000000..31b6dc7f --- /dev/null +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePingPassthrough.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019-2020 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.platform.sponge; + +import org.geysermc.common.ping.GeyserPingInfo; +import org.geysermc.connector.ping.IGeyserPingPassthrough; +import org.spongepowered.api.MinecraftVersion; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.event.SpongeEventFactory; +import org.spongepowered.api.event.cause.Cause; +import org.spongepowered.api.event.cause.EventContext; +import org.spongepowered.api.event.server.ClientPingServerEvent; +import org.spongepowered.api.network.status.StatusClient; + +import java.lang.reflect.Method; +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.util.Optional; + +public class GeyserSpongePingPassthrough implements IGeyserPingPassthrough { + + private static final GeyserStatusClient STATUS_CLIENT = new GeyserStatusClient(); + private static final Cause CAUSE = Cause.of(EventContext.empty(), Sponge.getServer()); + + private static Method SpongeStatusResponse_create; + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public GeyserPingInfo getPingInformation() { + // come on Sponge, this is in commons, why not expose it :( + ClientPingServerEvent event; + try { + if(SpongeStatusResponse_create == null) { + Class SpongeStatusResponse = Class.forName("org.spongepowered.common.network.status.SpongeStatusResponse"); + Class MinecraftServer = Class.forName("net.minecraft.server.MinecraftServer"); + SpongeStatusResponse_create = SpongeStatusResponse.getDeclaredMethod("create", MinecraftServer); + } + + Object response = SpongeStatusResponse_create.invoke(null, Sponge.getServer()); + event = SpongeEventFactory.createClientPingServerEvent(CAUSE, STATUS_CLIENT, (ClientPingServerEvent.Response) response); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + Sponge.getEventManager().post(event); + GeyserPingInfo geyserPingInfo = new GeyserPingInfo( + event.getResponse().getDescription().toPlain(), + event.getResponse().getPlayers().orElseThrow(IllegalStateException::new).getOnline(), + event.getResponse().getPlayers().orElseThrow(IllegalStateException::new).getMax()); + event.getResponse().getPlayers().get().getProfiles().forEach(player -> { + geyserPingInfo.addPlayer(player.getName().orElseThrow(IllegalStateException::new)); + }); + return geyserPingInfo; + } + + @SuppressWarnings("NullableProblems") + private static class GeyserStatusClient implements StatusClient { + + private static final InetSocketAddress FAKE_REMOTE = new InetSocketAddress(Inet4Address.getLoopbackAddress(), 69); + + @Override + public InetSocketAddress getAddress() { + return FAKE_REMOTE; + } + + @Override + public MinecraftVersion getVersion() { + return Sponge.getPlatform().getMinecraftVersion(); + } + + @Override + public Optional getVirtualHost() { + return Optional.empty(); + } + } +} diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java index 04a80adf..d226add7 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java @@ -34,6 +34,8 @@ import org.geysermc.connector.GeyserConfiguration; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; import org.geysermc.connector.command.CommandManager; +import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.utils.FileUtils; import org.geysermc.platform.sponge.command.GeyserSpongeCommandExecutor; import org.geysermc.platform.sponge.command.GeyserSpongeCommandManager; @@ -63,6 +65,7 @@ public class GeyserSpongePlugin implements GeyserBootstrap { private GeyserSpongeCommandManager geyserCommandManager; private GeyserSpongeConfiguration geyserConfig; private GeyserSpongeLogger geyserLogger; + private IGeyserPingPassthrough geyserSpongePingPassthrough; private GeyserConnector connector; @@ -108,8 +111,14 @@ public class GeyserSpongePlugin implements GeyserBootstrap { this.geyserLogger = new GeyserSpongeLogger(logger, geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); this.connector = GeyserConnector.start(PlatformType.SPONGE, this); - this.geyserCommandManager = new GeyserSpongeCommandManager(Sponge.getCommandManager(), connector); + if (geyserConfig.isLegacyPingPassthrough()) { + this.geyserSpongePingPassthrough = GeyserLegacyPingPassthrough.init(connector); + } else { + this.geyserSpongePingPassthrough = new GeyserSpongePingPassthrough(); + } + + this.geyserCommandManager = new GeyserSpongeCommandManager(Sponge.getCommandManager(), connector); Sponge.getCommandManager().register(this, new GeyserSpongeCommandExecutor(connector), "geyser"); } @@ -133,6 +142,11 @@ public class GeyserSpongePlugin implements GeyserBootstrap { return this.geyserCommandManager; } + @Override + public IGeyserPingPassthrough getGeyserPingPassthrough() { + return geyserSpongePingPassthrough; + } + @Listener public void onServerStart(GameStartedServerEvent event) { onEnable(); diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java index 0916f212..aa0d2392 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java @@ -26,10 +26,12 @@ package org.geysermc.platform.standalone; import org.geysermc.common.PlatformType; +import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; import org.geysermc.connector.GeyserConfiguration; import org.geysermc.connector.bootstrap.GeyserBootstrap; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.CommandManager; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.utils.FileUtils; import org.geysermc.platform.standalone.command.GeyserCommandManager; @@ -40,8 +42,9 @@ import java.util.UUID; public class GeyserStandaloneBootstrap implements GeyserBootstrap { private GeyserCommandManager geyserCommandManager; - private GeyserConfiguration geyserConfig; + private GeyserStandaloneConfiguration geyserConfig; private GeyserStandaloneLogger geyserLogger; + private IGeyserPingPassthrough geyserPingPassthrough; private GeyserConnector connector; @@ -66,6 +69,9 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { connector = GeyserConnector.start(PlatformType.STANDALONE, this); geyserCommandManager = new GeyserCommandManager(connector); + + geyserPingPassthrough = GeyserLegacyPingPassthrough.init(connector); + geyserLogger.start(); } @@ -89,4 +95,9 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { public CommandManager getGeyserCommandManager() { return geyserCommandManager; } + + @Override + public IGeyserPingPassthrough getGeyserPingPassthrough() { + return geyserPingPassthrough; + } } diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneConfiguration.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneConfiguration.java index 8c8ce376..bd029204 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneConfiguration.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneConfiguration.java @@ -50,8 +50,17 @@ public class GeyserStandaloneConfiguration implements GeyserConfiguration { @JsonProperty("command-suggestions") private boolean isCommandSuggestions; - @JsonProperty("ping-passthrough") - private boolean pingPassthrough; + @JsonProperty("passthrough-motd") + private boolean isPassthroughMotd; + + @JsonProperty("passthrough-player-counts") + private boolean isPassthroughPlayerCounts; + + @JsonProperty("legacy-ping-passthrough") + private boolean isLegacyPingPassthrough; + + @JsonProperty("ping-passthrough-interval") + private int pingPassthroughInterval; @JsonProperty("max-players") private int maxPlayers; diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityConfiguration.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityConfiguration.java index 685c21a3..aef0edaa 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityConfiguration.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityConfiguration.java @@ -55,8 +55,17 @@ public class GeyserVelocityConfiguration implements GeyserConfiguration { @JsonProperty("command-suggestions") private boolean commandSuggestions; - @JsonProperty("ping-passthrough") - private boolean pingPassthrough; + @JsonProperty("passthrough-motd") + private boolean isPassthroughMotd; + + @JsonProperty("passthrough-player-counts") + private boolean isPassthroughPlayerCounts; + + @JsonProperty("legacy-ping-passthrough") + private boolean isLegacyPingPassthrough; + + @JsonProperty("ping-passthrough-interval") + private int pingPassthroughInterval; @JsonProperty("max-players") private int maxPlayers; diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPingPassthrough.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPingPassthrough.java new file mode 100644 index 00000000..01be949b --- /dev/null +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPingPassthrough.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019-2020 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.platform.velocity; + +import com.velocitypowered.api.event.proxy.ProxyPingEvent; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.InboundConnection; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.server.ServerPing; +import lombok.AllArgsConstructor; +import net.kyori.text.TextComponent; +import net.kyori.text.serializer.legacy.LegacyComponentSerializer; +import org.geysermc.common.ping.GeyserPingInfo; +import org.geysermc.connector.ping.IGeyserPingPassthrough; + +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +@AllArgsConstructor +public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough { + + private static final GeyserInboundConnection FAKE_INBOUND_CONNECTION = new GeyserInboundConnection(); + + private final ProxyServer server; + + @Override + public GeyserPingInfo getPingInformation() { + ProxyPingEvent event; + try { + event = server.getEventManager().fire(new ProxyPingEvent(FAKE_INBOUND_CONNECTION, ServerPing.builder() + .description(server.getConfiguration().getMotdComponent()).onlinePlayers(server.getPlayerCount()) + .maximumPlayers(server.getConfiguration().getShowMaxPlayers()).build())).get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + GeyserPingInfo geyserPingInfo = new GeyserPingInfo( + LegacyComponentSerializer.INSTANCE.serialize(event.getPing().getDescription(), 'ยง'), + event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getOnline(), + event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getMax() + ); + event.getPing().getPlayers().get().getSample().forEach(player -> { + geyserPingInfo.addPlayer(player.getName()); + }); + return geyserPingInfo; + } + + private static class GeyserInboundConnection implements InboundConnection { + + private static final InetSocketAddress FAKE_REMOTE = new InetSocketAddress(Inet4Address.getLoopbackAddress(), 69); + + @Override + public InetSocketAddress getRemoteAddress() { + return FAKE_REMOTE; + } + + @Override + public Optional getVirtualHost() { + return Optional.empty(); + } + + @Override + public boolean isActive() { + return false; + } + + @Override + public ProtocolVersion getProtocolVersion() { + return ProtocolVersion.MAXIMUM_VERSION; + } + } + +} diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java index 9ad8ea9a..e7b44da5 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java @@ -38,6 +38,8 @@ import org.geysermc.common.PlatformType; import org.geysermc.connector.GeyserConfiguration; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; +import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.utils.FileUtils; import org.geysermc.platform.velocity.command.GeyserVelocityCommandExecutor; import org.geysermc.platform.velocity.command.GeyserVelocityCommandManager; @@ -63,6 +65,7 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { private GeyserVelocityCommandManager geyserCommandManager; private GeyserVelocityConfiguration geyserConfig; private GeyserVelocityLogger geyserLogger; + private IGeyserPingPassthrough geyserPingPassthrough; private GeyserConnector connector; @@ -99,6 +102,11 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { this.geyserCommandManager = new GeyserVelocityCommandManager(connector); this.commandManager.register(new GeyserVelocityCommandExecutor(connector), "geyser"); + if (geyserConfig.isLegacyPingPassthrough()) { + this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(connector); + } else { + this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer); + } } @Override @@ -121,6 +129,11 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { return this.geyserCommandManager; } + @Override + public IGeyserPingPassthrough getGeyserPingPassthrough() { + return geyserPingPassthrough; + } + @Subscribe public void onInit(ProxyInitializeEvent event) { onEnable(); diff --git a/common/src/main/java/org/geysermc/common/ping/GeyserPingInfo.java b/common/src/main/java/org/geysermc/common/ping/GeyserPingInfo.java new file mode 100644 index 00000000..40ef6da6 --- /dev/null +++ b/common/src/main/java/org/geysermc/common/ping/GeyserPingInfo.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2020 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.common.ping; + +import lombok.Data; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collection; + +@Data +public class GeyserPingInfo { + + public final String motd; + public final int currentPlayerCount; + public final int maxPlayerCount; + + @Getter + private Collection players = new ArrayList<>(); + + public void addPlayer(String username) { + players.add(username); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/GeyserConfiguration.java index 34edd8e0..8a39323b 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConfiguration.java @@ -32,7 +32,7 @@ import java.util.Map; public interface GeyserConfiguration { // Modify this when you update the config - int CURRENT_CONFIG_VERSION = 2; + int CURRENT_CONFIG_VERSION = 3; IBedrockConfiguration getBedrock(); @@ -42,7 +42,13 @@ public interface GeyserConfiguration { boolean isCommandSuggestions(); - boolean isPingPassthrough(); + boolean isPassthroughMotd(); + + boolean isPassthroughPlayerCounts(); + + boolean isLegacyPingPassthrough(); + + int getPingPassthroughInterval(); int getMaxPlayers(); diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 75c17b29..43b10053 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -39,7 +39,6 @@ import org.geysermc.connector.network.remote.RemoteServer; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.Translators; import org.geysermc.connector.network.translators.world.WorldManager; -import org.geysermc.connector.thread.PingPassthroughThread; import org.geysermc.connector.utils.DimensionUtils; import org.geysermc.connector.utils.DockerCheck; import org.geysermc.connector.utils.Toolbox; @@ -71,7 +70,6 @@ public class GeyserConnector { private boolean shuttingDown = false; private final ScheduledExecutorService generalThreadPool; - private PingPassthroughThread passthroughThread; private BedrockServer bedrockServer; private PlatformType platformType; @@ -111,10 +109,6 @@ public class GeyserConnector { remoteServer = new RemoteServer(config.getRemote().getAddress(), config.getRemote().getPort()); authType = AuthType.getByName(config.getRemote().getAuthType()); - passthroughThread = new PingPassthroughThread(this); - if (config.isPingPassthrough()) - generalThreadPool.scheduleAtFixedRate(passthroughThread, 1, 1, TimeUnit.SECONDS); - if (config.isAboveBedrockNetherBuilding()) DimensionUtils.changeBedrockNetherId(); // Apply End dimension ID workaround to Nether diff --git a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java index 24b338c8..24ce81cf 100644 --- a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java +++ b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java @@ -26,6 +26,7 @@ package org.geysermc.connector.bootstrap; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.GeyserConfiguration; import org.geysermc.connector.GeyserLogger; import org.geysermc.connector.command.CommandManager; @@ -67,6 +68,13 @@ public interface GeyserBootstrap { */ CommandManager getGeyserCommandManager(); + /** + * Returns the current PingPassthrough manager + * + * @return The current PingPassthrough manager + */ + IGeyserPingPassthrough getGeyserPingPassthrough(); + /** * Returns the current WorldManager * diff --git a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java index 8810cffb..29ba1567 100644 --- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java @@ -25,11 +25,12 @@ package org.geysermc.connector.network; -import com.github.steveice10.mc.protocol.data.status.ServerStatusInfo; -import com.nukkitx.protocol.bedrock.BedrockPong; -import com.nukkitx.protocol.bedrock.BedrockServerEventHandler; -import com.nukkitx.protocol.bedrock.BedrockServerSession; - +import com.github.steveice10.mc.protocol.data.message.Message; +import com.nukkitx.protocol.bedrock.*; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import org.geysermc.common.ping.GeyserPingInfo; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.GeyserConfiguration; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; @@ -39,7 +40,7 @@ import java.net.InetSocketAddress; public class ConnectorServerEventHandler implements BedrockServerEventHandler { - private GeyserConnector connector; + private final GeyserConnector connector; public ConnectorServerEventHandler(GeyserConnector connector) { this.connector = connector; @@ -56,29 +57,39 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { connector.getLogger().debug(inetSocketAddress + " has pinged you!"); GeyserConfiguration config = connector.getConfig(); - ServerStatusInfo serverInfo = connector.getPassthroughThread().getInfo(); + + GeyserPingInfo pingInfo = null; + if (config.isPassthroughMotd() || config.isPassthroughPlayerCounts()) { + IGeyserPingPassthrough pingPassthrough = connector.getBootstrap().getGeyserPingPassthrough(); + pingInfo = pingPassthrough.getPingInformation(); + } BedrockPong pong = new BedrockPong(); pong.setEdition("MCPE"); pong.setGameType("Default"); pong.setNintendoLimited(false); pong.setProtocolVersion(GeyserConnector.BEDROCK_PACKET_CODEC.getProtocolVersion()); - pong.setVersion(GeyserConnector.BEDROCK_PACKET_CODEC.getMinecraftVersion()); + pong.setVersion(null); // Server tries to connect either way and it looks better pong.setIpv4Port(config.getBedrock().getPort()); - if (connector.getConfig().isPingPassthrough() && serverInfo != null) { - String[] motd = MessageUtils.getBedrockMessage(serverInfo.getDescription()).split("\n"); + + if (config.isPassthroughMotd() && pingInfo != null && pingInfo.motd != null) { + String[] motd = MessageUtils.getBedrockMessage(Message.fromString(pingInfo.motd)).split("\n"); String mainMotd = motd[0]; // First line of the motd. String subMotd = (motd.length != 1) ? motd[1] : ""; // Second line of the motd if present, otherwise blank. pong.setMotd(mainMotd.trim()); pong.setSubMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit. - pong.setPlayerCount(serverInfo.getPlayerInfo().getOnlinePlayers()); - pong.setMaximumPlayerCount(serverInfo.getPlayerInfo().getMaxPlayers()); + } else { + pong.setMotd(config.getBedrock().getMotd1()); + pong.setSubMotd(config.getBedrock().getMotd2()); + } + + if (config.isPassthroughPlayerCounts() && pingInfo != null) { + pong.setPlayerCount(pingInfo.currentPlayerCount); + pong.setMaximumPlayerCount(pingInfo.maxPlayerCount); } else { pong.setPlayerCount(connector.getPlayers().size()); pong.setMaximumPlayerCount(config.getMaxPlayers()); - pong.setMotd(config.getBedrock().getMotd1()); - pong.setMotd(config.getBedrock().getMotd2()); } //Bedrock will not even attempt a connection if the client thinks the server is full @@ -105,4 +116,9 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { }); bedrockServerSession.setPacketCodec(GeyserConnector.BEDROCK_PACKET_CODEC); } + + @Override + public void onUnhandledDatagram(ChannelHandlerContext ctx, DatagramPacket packet) { + new QueryPacketHandler(connector, packet.sender(), packet.content()); + } } \ No newline at end of file diff --git a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java new file mode 100644 index 00000000..061e0f86 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2019-2020 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.connector.network; + +import com.github.steveice10.mc.protocol.data.message.Message; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.geysermc.common.ping.GeyserPingInfo; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.utils.MessageUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +public class QueryPacketHandler { + + public static final byte HANDSHAKE = 0x09; + public static final byte STATISTICS = 0x00; + + private GeyserConnector connector; + private InetSocketAddress sender; + private byte type; + private int sessionId; + private byte[] token; + + /** + * The Query packet handler instance + * @param connector Geyser Connector + * @param sender The Sender IP/Port for the Query + * @param buffer The Query data + */ + public QueryPacketHandler(GeyserConnector connector, InetSocketAddress sender, ByteBuf buffer) { + if(!isQueryPacket(buffer)) + return; + + this.connector = connector; + this.sender = sender; + this.type = buffer.readByte(); + this.sessionId = buffer.readInt(); + + regenerateToken(); + handle(); + } + + /** + * Checks the packet is in fact a query packet + * @param buffer Query data + * @return if the packet is a query packet + */ + private boolean isQueryPacket(ByteBuf buffer) { + return (buffer.readableBytes() >= 2) ? buffer.readUnsignedShort() == 65277 : false; + } + + /** + * Handles the query + */ + private void handle() { + switch (type) { + case HANDSHAKE: + sendToken(); + case STATISTICS: + sendQueryData(); + } + } + + /** + * Sends the token to the sender + */ + private void sendToken() { + ByteBuf reply = ByteBufAllocator.DEFAULT.ioBuffer(10); + reply.writeByte(HANDSHAKE); + reply.writeInt(sessionId); + reply.writeBytes(getTokenString(this.token, this.sender.getAddress())); + reply.writeByte(0); + + sendPacket(reply); + } + + /** + * Sends the query data to the sender + */ + private void sendQueryData() { + ByteBuf reply = ByteBufAllocator.DEFAULT.ioBuffer(64); + reply.writeByte(STATISTICS); + reply.writeInt(sessionId); + + // Game Info + reply.writeBytes(getGameData()); + + // Players + reply.writeBytes(getPlayers()); + + sendPacket(reply); + } + + /** + * Gets the game data for the query + * @return the game data for the query + */ + private byte[] getGameData() { + ByteArrayOutputStream query = new ByteArrayOutputStream(); + + GeyserPingInfo pingInfo = null; + String motd; + String currentPlayerCount; + String maxPlayerCount; + + if (connector.getConfig().isPassthroughMotd() || connector.getConfig().isPassthroughPlayerCounts()) { + pingInfo = connector.getBootstrap().getGeyserPingPassthrough().getPingInformation(); + } + + if (connector.getConfig().isPassthroughMotd() && pingInfo != null) { + String[] javaMotd = MessageUtils.getBedrockMessage(Message.fromString(pingInfo.motd)).split("\n"); + motd = javaMotd[0].trim(); // First line of the motd. + } else { + motd = connector.getConfig().getBedrock().getMotd1(); + } + + // If passthrough player counts is enabled lets get players from the server + if (connector.getConfig().isPassthroughPlayerCounts() && pingInfo != null) { + currentPlayerCount = String.valueOf(pingInfo.currentPlayerCount); + maxPlayerCount = String.valueOf(pingInfo.maxPlayerCount); + } else { + currentPlayerCount = String.valueOf(connector.getPlayers().size()); + maxPlayerCount = String.valueOf(connector.getConfig().getMaxPlayers()); + } + + // Create a hashmap of all game data needed in the query + Map gameData = new HashMap(); + gameData.put("hostname", motd); + gameData.put("gametype", "SMP"); + gameData.put("game_id", "MINECRAFT"); + gameData.put("version", GeyserConnector.BEDROCK_PACKET_CODEC.getMinecraftVersion()); + gameData.put("plugins", ""); + gameData.put("map", GeyserConnector.NAME); + gameData.put("numplayers", currentPlayerCount); + gameData.put("maxplayers", maxPlayerCount); + gameData.put("hostport", String.valueOf(connector.getConfig().getBedrock().getPort())); + gameData.put("hostip", connector.getConfig().getBedrock().getAddress()); + + try { + // Blank Buffer Bytes + query.write("GeyserMC".getBytes()); + query.write((byte) 0x00); + query.write((byte) 128); + query.write((byte) 0x00); + + // Fills the game data + for(Map.Entry entry : gameData.entrySet()) { + query.write(entry.getKey().getBytes()); + query.write((byte) 0x00); + query.write(entry.getValue().getBytes()); + query.write((byte) 0x00); + } + + // Final byte to show the end of the game data + query.write(new byte[]{0x00, 0x01}); + return query.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + return new byte[0]; + } + } + + private byte[] getPlayers() { + ByteArrayOutputStream query = new ByteArrayOutputStream(); + + GeyserPingInfo pingInfo = null; + if (connector.getConfig().isPassthroughMotd() || connector.getConfig().isPassthroughPlayerCounts()) { + pingInfo = connector.getBootstrap().getGeyserPingPassthrough().getPingInformation(); + } + + try { + // Start the player section + query.write("player_".getBytes()); + query.write(new byte[]{0x00, 0x00}); + + // Fill player names + if(pingInfo != null) { + for (String username : pingInfo.getPlayers()) { + query.write(username.getBytes()); + query.write((byte) 0x00); + } + } + + // Final byte to show the end of the player data + query.write((byte) 0x00); + return query.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + return new byte[0]; + } + } + + /** + * Sends a packet to the sender + * @param data packet data + */ + private void sendPacket(ByteBuf data) { + connector.getBedrockServer().getRakNet().send(sender, data); + } + + /** + * Regenerates a token + */ + public void regenerateToken() { + byte[] token = new byte[16]; + for (int i = 0; i < 16; i++) { + token[i] = (byte) new Random().nextInt(255); + } + + this.token = token; + } + + /** + * Gets an MD5 token for the current IP/Port. + * This should reset every 30 seconds but a new one is generated per instance + * Seems wasteful to code something in to clear it when it has no use. + * @param token the token + * @param address the address + * @return an MD5 token for the current IP/Port + */ + public static byte[] getTokenString(byte[] token, InetAddress address) { + try { + MessageDigest digest = MessageDigest.getInstance("MD5"); + digest.update(address.toString().getBytes(StandardCharsets.UTF_8)); + digest.update(token); + return Arrays.copyOf(digest.digest(), 4); + } catch (NoSuchAlgorithmException e) { + return ByteBuffer.allocate(4).putInt(ThreadLocalRandom.current().nextInt()).array(); + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/ping/GeyserLegacyPingPassthrough.java b/connector/src/main/java/org/geysermc/connector/ping/GeyserLegacyPingPassthrough.java new file mode 100644 index 00000000..ea0d67c2 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/ping/GeyserLegacyPingPassthrough.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019-2020 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.connector.ping; + +import com.github.steveice10.mc.protocol.MinecraftConstants; +import com.github.steveice10.mc.protocol.MinecraftProtocol; +import com.github.steveice10.mc.protocol.data.SubProtocol; +import com.github.steveice10.mc.protocol.data.status.handler.ServerInfoHandler; +import com.github.steveice10.packetlib.Client; +import com.github.steveice10.packetlib.tcp.TcpSessionFactory; +import org.geysermc.common.ping.GeyserPingInfo; +import org.geysermc.connector.GeyserConfiguration; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.GeyserLogger; + +import java.util.concurrent.TimeUnit; + +public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runnable { + + private GeyserConnector connector; + + public GeyserLegacyPingPassthrough(GeyserConnector connector) { + this.connector = connector; + this.pingInfo = new GeyserPingInfo(null, 0, 0); + } + + private GeyserPingInfo pingInfo; + + private Client client; + + /** + * Start legacy ping passthrough thread + * @param connector GeyserConnector + * @return GeyserPingPassthrough, or null if not initialized + */ + public static IGeyserPingPassthrough init(GeyserConnector connector) { + if (connector.getConfig().isPassthroughMotd() || connector.getConfig().isPassthroughPlayerCounts()) { + GeyserLegacyPingPassthrough pingPassthrough = new GeyserLegacyPingPassthrough(connector); + // Ensure delay is not zero + int interval = (connector.getConfig().getPingPassthroughInterval() == 0) ? 1 : connector.getConfig().getPingPassthroughInterval(); + connector.getLogger().debug("Scheduling ping passthrough at an interval of " + interval + " second(s)."); + connector.getGeneralThreadPool().scheduleAtFixedRate(pingPassthrough, 1, interval, TimeUnit.SECONDS); + return pingPassthrough; + } + return null; + } + + @Override + public GeyserPingInfo getPingInformation() { + return pingInfo; + } + + @Override + public void run() { + try { + this.client = new Client(connector.getConfig().getRemote().getAddress(), connector.getConfig().getRemote().getPort(), new MinecraftProtocol(SubProtocol.STATUS), new TcpSessionFactory()); + this.client.getSession().setFlag(MinecraftConstants.SERVER_INFO_HANDLER_KEY, (ServerInfoHandler) (session, info) -> { + this.pingInfo = new GeyserPingInfo(info.getDescription().getFullText(), info.getPlayerInfo().getOnlinePlayers(), info.getPlayerInfo().getMaxPlayers()); + this.client.getSession().disconnect(null); + }); + + client.getSession().connect(); + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/ping/IGeyserPingPassthrough.java b/connector/src/main/java/org/geysermc/connector/ping/IGeyserPingPassthrough.java new file mode 100644 index 00000000..7bc842df --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/ping/IGeyserPingPassthrough.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2020 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.connector.ping; + +import org.geysermc.common.ping.GeyserPingInfo; + +/** + * Interface that retrieves ping passthrough information from the Java server + */ +public interface IGeyserPingPassthrough { + + /** + * Get the MOTD of the server displayed on the multiplayer screen + * @return string of the MOTD + */ + GeyserPingInfo getPingInformation(); + +} diff --git a/connector/src/main/java/org/geysermc/connector/thread/PingPassthroughThread.java b/connector/src/main/java/org/geysermc/connector/thread/PingPassthroughThread.java deleted file mode 100644 index fa87809b..00000000 --- a/connector/src/main/java/org/geysermc/connector/thread/PingPassthroughThread.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2019-2020 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.connector.thread; - -import com.github.steveice10.mc.protocol.MinecraftConstants; -import com.github.steveice10.mc.protocol.MinecraftProtocol; -import com.github.steveice10.mc.protocol.data.SubProtocol; -import com.github.steveice10.mc.protocol.data.status.ServerStatusInfo; -import com.github.steveice10.mc.protocol.data.status.handler.ServerInfoHandler; -import com.github.steveice10.packetlib.Client; -import com.github.steveice10.packetlib.tcp.TcpSessionFactory; -import lombok.Getter; -import org.geysermc.connector.GeyserConnector; - -public class PingPassthroughThread implements Runnable { - - private GeyserConnector connector; - - public PingPassthroughThread(GeyserConnector connector) { - this.connector = connector; - } - - @Getter - private ServerStatusInfo info; - - private Client client; - - @Override - public void run() { - try { - this.client = new Client(connector.getConfig().getRemote().getAddress(), connector.getConfig().getRemote().getPort(), new MinecraftProtocol(SubProtocol.STATUS), new TcpSessionFactory()); - this.client.getSession().setFlag(MinecraftConstants.SERVER_INFO_HANDLER_KEY, (ServerInfoHandler) (session, info) -> { - this.info = info; - this.client.getSession().disconnect(null); - }); - - client.getSession().connect(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } -} diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index d12b7648..931e0a8d 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -12,7 +12,7 @@ bedrock: address: 0.0.0.0 # The port that will listen for connections port: 19132 - # The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients + # The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. Irrelevant if "passthrough-motd" is set to true motd1: "GeyserMC" motd2: "Another GeyserMC forced host." remote: @@ -45,8 +45,17 @@ floodgate-key-file: public-key.pem # Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients. command-suggestions: true -# Relay the MOTD, player count and max players from the remote server -ping-passthrough: false +# The following two options enable "ping passthrough" - the MOTD and/or player count gets retrieved from the Java server. +# Relay the MOTD from the remote server to Bedrock players. +passthrough-motd: false +# Relay the player count and max players from the remote server to Bedrock players. +passthrough-player-counts: false +# Enable LEGACY ping passthrough. There is no need to enable this unless your MOTD or player count does not appear properly. +# This option does nothing on standalone. +legacy-ping-passthrough: false +# How often to ping the remote server, in seconds. Only relevant for standalone or legacy ping passthrough. +# Increase if you are getting BrokenPipe errors. +ping-passthrough-interval: 3 # Maximum amount of players that can connect max-players: 100 @@ -94,4 +103,4 @@ metrics: uuid: generateduuid # DO NOT TOUCH! -config-version: 1 +config-version: 3 diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings index a67cc940..a7963d0a 160000 --- a/connector/src/main/resources/mappings +++ b/connector/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit a67cc940c0d47874c833ffeb58f38e33eabfcc33 +Subproject commit a7963d0a0236b1c47eea21718ac50706139d90cc