diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java index 81a7b8297..402167151 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -1,5 +1,7 @@ package com.velocitypowered.api.command; +import java.util.concurrent.CompletableFuture; + /** * Represents an interface to register a command executor with the proxy. */ @@ -34,11 +36,52 @@ public interface CommandManager { void unregister(String alias); /** - * Attempts to execute a command from the specified {@code cmdLine}. + * Calls CommandExecuteEvent and attempts to execute a command using the specified {@code cmdLine} + * in a blocking fashion. * * @param source the command's source * @param cmdLine the command to run * @return true if the command was found and executed, false if it was not + * + * @deprecated This method will block current thread during event call and command execution. + * Prefer {@link #executeAsync(CommandSource, String)} instead. */ + @Deprecated boolean execute(CommandSource source, String cmdLine); + + /** + * Attempts to execute a command using the specified {@code cmdLine} in a blocking fashion without + * calling CommandExecuteEvent. + * + * @param source the command's source + * @param cmdLine the command to run + * @return true if the command was found and executed, false if it was not + * + * @deprecated This method will block current thread during event and command execution. + * Prefer {@link #executeImmediatelyAsync(CommandSource, String)} instead. + */ + @Deprecated + boolean executeImmediately(CommandSource source, String cmdLine); + + /** + * Calls CommandExecuteEvent and attempts to execute a command from the specified {@code cmdLine} + * async. + * + * @param source the command's source + * @param cmdLine the command to run + * @return A future that will be completed with the result of the command execution. + * Can be completed exceptionally if exception was thrown during execution. + */ + CompletableFuture executeAsync(CommandSource source, String cmdLine); + + /** + * Attempts to execute a command from the specified {@code cmdLine} async + * without calling CommandExecuteEvent. + * + * @param source the command's source + * @param cmdLine the command to run + * @return A future that will be completed with the result of the command execution. + * Can be completed exceptionally if exception was thrown during execution. + */ + CompletableFuture executeImmediatelyAsync(CommandSource source, String cmdLine); } diff --git a/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java b/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java new file mode 100644 index 000000000..d08a323f6 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java @@ -0,0 +1,143 @@ +package com.velocitypowered.api.event.command; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult; +import java.util.Optional; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This event is fired when someone executing command. + */ +public final class CommandExecuteEvent implements ResultedEvent { + + private final CommandSource commandSource; + private final String command; + private CommandResult result; + + /** + * Constructs a CommandExecuteEvent. + * @param commandSource the source executing the command + * @param command the command being executed without first slash + */ + public CommandExecuteEvent(CommandSource commandSource, String command) { + this.commandSource = Preconditions.checkNotNull(commandSource, "commandSource"); + this.command = Preconditions.checkNotNull(command, "command"); + this.result = CommandResult.allowed(); + } + + public CommandSource getCommandSource() { + return commandSource; + } + + /** + * Gets the original command being executed without first slash. + * @return the original command being executed + */ + public String getCommand() { + return command; + } + + @Override + public CommandResult getResult() { + return result; + } + + @Override + public void setResult(CommandResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } + + @Override + public String toString() { + return "CommandExecuteEvent{" + + "commandSource=" + commandSource + + ", command=" + command + + ", result=" + result + + '}'; + } + + /** + * Represents the result of the {@link CommandExecuteEvent}. + */ + public static final class CommandResult implements Result { + + private static final CommandResult ALLOWED = new CommandResult(true, false,null); + private static final CommandResult DENIED = new CommandResult(false, false,null); + private static final CommandResult FORWARD_TO_SERVER = new CommandResult(false, true, null); + + private @Nullable String command; + private final boolean status; + private final boolean forward; + + private CommandResult(boolean status, boolean forward, @Nullable String command) { + this.status = status; + this.forward = forward; + this.command = command; + } + + public Optional getCommand() { + return Optional.ofNullable(command); + } + + public boolean isForwardToServer() { + return forward; + } + + @Override + public boolean isAllowed() { + return status; + } + + @Override + public String toString() { + return status ? "allowed" : "denied"; + } + + /** + * Allows the command to be sent, without modification. + * @return the allowed result + */ + public static CommandResult allowed() { + return ALLOWED; + } + + /** + * Prevents the command from being executed. + * @return the denied result + */ + public static CommandResult denied() { + return DENIED; + } + + /** + * Prevents the command from being executed, but forward command to server. + * @return the forward result + */ + public static CommandResult forwardToServer() { + return FORWARD_TO_SERVER; + } + + /** + * Prevents the command from being executed on proxy, but forward command to server. + * @param newCommand the command without first slash to use instead + * @return a result with a new command being forwarded to server + */ + public static CommandResult forwardToServer(@NonNull String newCommand) { + Preconditions.checkNotNull(newCommand, "newCommand"); + return new CommandResult(false, true, newCommand); + } + + /** + * Allows the command to be executed, but silently replaced old command with another. + * @param newCommand the command to use instead without first slash + * @return a result with a new command + */ + public static CommandResult command(@NonNull String newCommand) { + Preconditions.checkNotNull(newCommand, "newCommand"); + return new CommandResult(true, false, newCommand); + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/GameProfileRequestEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/GameProfileRequestEvent.java index 6574f9d7b..4c209056a 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/GameProfileRequestEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/GameProfileRequestEvent.java @@ -60,13 +60,11 @@ public final class GameProfileRequestEvent { } /** - * Sets the game profile to use for this connection. It is invalid to use this method on an - * online-mode connection. + * Sets the game profile to use for this connection. * * @param gameProfile the profile for this connection, {@code null} uses the original profile */ public void setGameProfile(@Nullable GameProfile gameProfile) { - Preconditions.checkState(!onlineMode, "Profiles can not be faked in online mode!"); this.gameProfile = gameProfile; } diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java index 6b8b7bba1..ef77c96a4 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java @@ -70,6 +70,10 @@ public final class PlayerChatEvent implements ResultedEvent getMessage() { + return Optional.ofNullable(message); + } + @Override public boolean isAllowed() { return status; @@ -96,10 +100,6 @@ public final class PlayerChatEvent implements ResultedEvent getMessage() { - return Optional.ofNullable(message); - } - /** * Allows the message to be sent, but silently replaced with another. * @param message the message to use instead @@ -110,6 +110,4 @@ public final class PlayerChatEvent implements ResultedEventvelocity.toml. This method does * not return all the servers currently in memory, although in most cases it diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index ad80a9161..036956882 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -44,6 +44,8 @@ import com.velocitypowered.proxy.util.bossbar.VelocityBossBar; import com.velocitypowered.proxy.util.ratelimit.Ratelimiter; import com.velocitypowered.proxy.util.ratelimit.Ratelimiters; import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import java.io.IOException; import java.net.InetSocketAddress; @@ -93,7 +95,7 @@ public class VelocityServer implements ProxyServer { private @MonotonicNonNull VelocityConfiguration configuration; private @MonotonicNonNull KeyPair serverKeyPair; private final ServerMap servers; - private final VelocityCommandManager commandManager = new VelocityCommandManager(); + private final VelocityCommandManager commandManager; private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false); private boolean shutdown = false; private final VelocityPluginManager pluginManager; @@ -109,6 +111,7 @@ public class VelocityServer implements ProxyServer { VelocityServer(final ProxyOptions options) { pluginManager = new VelocityPluginManager(this); eventManager = new VelocityEventManager(pluginManager); + commandManager = new VelocityCommandManager(eventManager); scheduler = new VelocityScheduler(pluginManager); console = new VelocityConsole(this); cm = new ConnectionManager(this); @@ -268,6 +271,10 @@ public class VelocityServer implements ProxyServer { return this.cm.createWorker(group); } + public ChannelInitializer getBackendChannelInitializer() { + return this.cm.backendChannelInitializer.get(); + } + public boolean isShutdown() { return shutdown; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java index b79d955bf..1e21e0ab7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java @@ -12,8 +12,6 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -25,6 +23,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; public class ServerCommand implements Command { + public static final int MAX_SERVERS_TO_LIST = 50; private final ProxyServer server; public ServerCommand(ProxyServer server) { @@ -61,10 +60,16 @@ public class ServerCommand implements Command { executor.sendMessage(of("You are currently connected to " + currentServer + ".", TextColor.YELLOW)); + List servers = BuiltinCommandUtil.sortedServerList(server); + if (servers.size() > MAX_SERVERS_TO_LIST) { + executor.sendMessage(of("Too many servers to list. Tab-complete to show all servers.", + TextColor.RED)); + return; + } + // Assemble the list of servers as components TextComponent.Builder serverListBuilder = TextComponent.builder("Available servers: ") .color(TextColor.YELLOW); - List servers = BuiltinCommandUtil.sortedServerList(server); for (int i = 0; i < servers.size(); i++) { RegisteredServer rs = servers.get(i); serverListBuilder.append(formatServerComponent(currentServer, rs)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index 4b85bdef6..2b3975f1a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -7,16 +7,26 @@ import com.velocitypowered.api.command.Command; import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.RawCommand; +import com.velocitypowered.api.event.command.CommandExecuteEvent; +import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult; +import com.velocitypowered.proxy.plugin.VelocityEventManager; + import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; public class VelocityCommandManager implements CommandManager { private final Map commands = new HashMap<>(); + private final VelocityEventManager eventManager; + + public VelocityCommandManager(VelocityEventManager eventManager) { + this.eventManager = eventManager; + } @Override @Deprecated @@ -47,9 +57,36 @@ public class VelocityCommandManager implements CommandManager { this.commands.remove(alias.toLowerCase(Locale.ENGLISH)); } + /** + * Calls CommandExecuteEvent. + * @param source the command's source + * @param cmd the command + * @return CompletableFuture of event + */ + public CompletableFuture callCommandEvent(CommandSource source, String cmd) { + Preconditions.checkNotNull(source, "source"); + Preconditions.checkNotNull(cmd, "cmd"); + return eventManager.fire(new CommandExecuteEvent(source, cmd)); + } + @Override public boolean execute(CommandSource source, String cmdLine) { - Preconditions.checkNotNull(source, "invoker"); + Preconditions.checkNotNull(source, "source"); + Preconditions.checkNotNull(cmdLine, "cmdLine"); + + CommandExecuteEvent event = callCommandEvent(source, cmdLine).join(); + CommandResult commandResult = event.getResult(); + if (commandResult.isForwardToServer() || !commandResult.isAllowed()) { + return false; + } + cmdLine = commandResult.getCommand().orElse(event.getCommand()); + + return executeImmediately(source, cmdLine); + } + + @Override + public boolean executeImmediately(CommandSource source, String cmdLine) { + Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); String alias = cmdLine; @@ -75,6 +112,40 @@ public class VelocityCommandManager implements CommandManager { } } + + @Override + public CompletableFuture executeAsync(CommandSource source, String cmdLine) { + CompletableFuture result = new CompletableFuture<>(); + callCommandEvent(source, cmdLine).thenAccept(event -> { + CommandResult commandResult = event.getResult(); + if (commandResult.isForwardToServer() || !commandResult.isAllowed()) { + result.complete(false); + } + String command = commandResult.getCommand().orElse(event.getCommand()); + try { + result.complete(executeImmediately(source, command)); + } catch (Exception e) { + result.completeExceptionally(e); + } + }); + return result; + } + + @Override + public CompletableFuture executeImmediatelyAsync(CommandSource source, String cmdLine) { + Preconditions.checkNotNull(source, "source"); + Preconditions.checkNotNull(cmdLine, "cmdLine"); + CompletableFuture result = new CompletableFuture<>(); + eventManager.getService().execute(() -> { + try { + result.complete(executeImmediately(source, cmdLine)); + } catch (Exception e) { + result.completeExceptionally(e); + } + }); + return result; + } + public boolean hasCommand(String command) { return commands.containsKey(command); } @@ -167,7 +238,14 @@ public class VelocityCommandManager implements CommandManager { if (line.isEmpty()) { return new String[0]; } - return line.trim().split(" ", -1); + + String[] trimmed = line.trim().split(" ", -1); + if (line.endsWith(" ") && !line.trim().isEmpty()) { + // To work around a 1.13+ specific bug we have to inject a space at the end of the arguments + trimmed = Arrays.copyOf(trimmed, trimmed.length + 1); + trimmed[trimmed.length - 1] = ""; + } + return trimmed; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 806ef44ce..3b83c4380 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -54,6 +54,14 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi @ConfigKey("online-mode") private boolean onlineMode = true; + @Comment({ + "If client's ISP/AS sent from this proxy is different from the one from Mojang's", + "authentication server, the player is kicked. This disallows some VPN and proxy", + "connections but is a weak form of protection." + }) + @ConfigKey("prevent-client-proxy-connections") + private boolean preventClientProxyConnections = false; + @Comment({ "Should we forward IP addresses and other data to backend servers?", "Available options:", @@ -328,6 +336,11 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi return onlineMode; } + @Override + public boolean shouldPreventClientProxyConnections() { + return preventClientProxyConnections; + } + public PlayerInfoForwarding getPlayerInfoForwardingMode() { return playerInfoForwardingMode; } @@ -402,6 +415,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi return pingPassthrough; } + public boolean isBungeePluginChannelEnabled() { + return advanced.isBungeePluginMessageChannel(); + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -654,6 +671,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi @ConfigKey("tcp-fast-open") private boolean tcpFastOpen = false; + @Comment("Enables BungeeCord plugin messaging channel support on Velocity.") + @ConfigKey("bungee-plugin-message-channel") + private boolean bungeePluginMessageChannel = true; + private Advanced() { } @@ -666,6 +687,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi this.readTimeout = toml.getLong("read-timeout", 30000L).intValue(); this.proxyProtocol = toml.getBoolean("proxy-protocol", false); this.tcpFastOpen = toml.getBoolean("tcp-fast-open", false); + this.bungeePluginMessageChannel = toml.getBoolean("bungee-plugin-message-channel", true); } } @@ -697,6 +719,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi return tcpFastOpen; } + public boolean isBungeePluginMessageChannel() { + return bungeePluginMessageChannel; + } + @Override public String toString() { return "Advanced{" @@ -707,6 +733,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi + ", readTimeout=" + readTimeout + ", proxyProtocol=" + proxyProtocol + ", tcpFastOpen=" + tcpFastOpen + + ", bungeePluginMessageChannel=" + bungeePluginMessageChannel + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java index 678c77e8f..deb5b2304 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java @@ -15,6 +15,7 @@ import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import java.util.Optional; import java.util.StringJoiner; import net.kyori.text.serializer.legacy.LegacyComponentSerializer; @@ -43,8 +44,11 @@ class BungeeCordMessageResponder { String playerName = in.readUTF(); String serverName = in.readUTF(); - proxy.getPlayer(playerName).flatMap(player -> proxy.getServer(serverName)) - .ifPresent(server -> player.createConnectionRequest(server).fireAndForget()); + Optional referencedPlayer = proxy.getPlayer(playerName); + Optional referencedServer = proxy.getServer(serverName); + if (referencedPlayer.isPresent() && referencedServer.isPresent()) { + referencedPlayer.get().createConnectionRequest(referencedServer.get()).fireAndForget(); + } } private void processIp(ByteBufDataInput in) { @@ -269,6 +273,10 @@ class BungeeCordMessageResponder { } boolean process(PluginMessage message) { + if (!proxy.getConfiguration().isBungeePluginChannelEnabled()) { + return false; + } + if (!MODERN_CHANNEL.getId().equals(message.getChannel()) && !LEGACY_CHANNEL.getId() .equals(message.getChannel())) { return false; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index a7cf3ed82..b91522507 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -80,22 +80,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, // Note: we use the event loop for the connection the player is on. This reduces context // switches. server.createBootstrap(proxyPlayer.getConnection().eventLoop()) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) throws Exception { - ch.pipeline() - .addLast(READ_TIMEOUT, - new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), - TimeUnit.MILLISECONDS)) - .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) - .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) - .addLast(FLOW_HANDLER, new FlowControlHandler()) - .addLast(MINECRAFT_DECODER, - new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) - .addLast(MINECRAFT_ENCODER, - new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); - } - }) + .handler(server.getBackendChannelInitializer()) .connect(registeredServer.getServerInfo().getAddress()) .addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 0ab5ce84f..78a44a68b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -4,6 +4,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; +import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; @@ -123,17 +124,30 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String msg = packet.getMessage(); if (msg.startsWith("/")) { - try { - if (!server.getCommandManager().execute(player, msg.substring(1))) { - return false; - } - } catch (Exception e) { - logger.info("Exception occurred while running command for {}", player.getUsername(), - e); - player.sendMessage( - TextComponent.of("An error occurred while running this command.", TextColor.RED)); - return true; - } + + server.getCommandManager().callCommandEvent(player, msg.substring(1)) + .thenAcceptAsync(event -> { + CommandExecuteEvent.CommandResult commandResult = event.getResult(); + Optional eventCommand = event.getResult().getCommand(); + String command = eventCommand.orElse(event.getCommand()); + if (commandResult.isForwardToServer()) { + smc.write(Chat.createServerbound("/" + command)); + return; + } + if (commandResult.isAllowed()) { + try { + if (!server.getCommandManager().executeImmediately(player, command)) { + smc.write(Chat.createServerbound("/" + command)); + } + } catch (Exception e) { + logger.info("Exception occurred while running command for {}", player.getUsername(), + e); + player.sendMessage( + TextComponent.of("An error occurred while running this command.", + TextColor.RED)); + } + } + }, smc.eventLoop()); } else { PlayerChatEvent event = new PlayerChatEvent(player, msg); server.getEventManager().fire(event) @@ -371,12 +385,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private boolean handleCommandTabComplete(TabCompleteRequest packet) { // In 1.13+, we need to do additional work for the richer suggestions available. String command = packet.getCommand().substring(1); - int spacePos = command.indexOf(' '); - if (spacePos == -1) { - spacePos = command.length(); + int commandEndPosition = command.indexOf(' '); + if (commandEndPosition == -1) { + commandEndPosition = command.length(); } - String commandLabel = command.substring(0, spacePos); + String commandLabel = command.substring(0, commandEndPosition); if (!server.getCommandManager().hasCommand(commandLabel)) { if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { // Outstanding tab completes are recorded for use with 1.12 clients and below to provide diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index 6fb58ffea..e367174c9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java @@ -50,7 +50,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class); private static final String MOJANG_HASJOINED_URL = - "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s"; + "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s"; private final VelocityServer server; private final MinecraftConnection mcConnection; @@ -96,8 +96,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler { String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString(); String url = String.format(MOJANG_HASJOINED_URL, - urlFormParameterEscaper().escape(login.getUsername()), serverId, - urlFormParameterEscaper().escape(playerIp)); + urlFormParameterEscaper().escape(login.getUsername()), serverId); + + if (server.getConfiguration().shouldPreventClientProxyConnections()) { + url += "&ip=" + urlFormParameterEscaper().escape(playerIp); + } ListenableFuture hasJoinedResponse = server.getAsyncHttpClient().prepareGet(url) .execute(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java index 4b9d38aee..1ccc0c101 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -8,6 +8,8 @@ import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.proxy.VelocityServer; import java.util.List; +import java.util.concurrent.CompletableFuture; + import net.kyori.text.Component; import net.kyori.text.TextComponent; import net.kyori.text.format.TextColor; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java new file mode 100644 index 000000000..cb261fc67 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java @@ -0,0 +1,45 @@ +package com.velocitypowered.proxy.network; + +import static com.velocitypowered.proxy.network.Connections.FLOW_HANDLER; +import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER; +import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER; +import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER; +import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; +import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; + +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.handler.flow.FlowControlHandler; +import io.netty.handler.timeout.ReadTimeoutHandler; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("WeakerAccess") +public class BackendChannelInitializer extends ChannelInitializer { + + private final VelocityServer server; + + public BackendChannelInitializer(VelocityServer server) { + this.server = server; + } + + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline() + .addLast(READ_TIMEOUT, + new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), + TimeUnit.MILLISECONDS)) + .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) + .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(FLOW_HANDLER, new FlowControlHandler()) + .addLast(MINECRAFT_DECODER, + new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) + .addLast(MINECRAFT_ENCODER, + new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializerHolder.java b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializerHolder.java new file mode 100644 index 000000000..7da1ab32d --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializerHolder.java @@ -0,0 +1,35 @@ +package com.velocitypowered.proxy.network; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import java.util.function.Supplier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class BackendChannelInitializerHolder implements Supplier> { + + private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class); + private ChannelInitializer initializer; + + BackendChannelInitializerHolder(final ChannelInitializer initializer) { + this.initializer = initializer; + } + + @Override + public ChannelInitializer get() { + return this.initializer; + } + + /** + * Sets the channel initializer. + * + * @param initializer the new initializer to use + * @deprecated Internal implementation detail + */ + @Deprecated + public void set(final ChannelInitializer initializer) { + LOGGER.warn("The backend channel initializer has been replaced by {}", + Thread.currentThread().getStackTrace()[2]); + this.initializer = initializer; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index 765af2261..649830544 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -42,10 +42,12 @@ public final class ConnectionManager { private final EventLoopGroup bossGroup; private final EventLoopGroup workerGroup; private final VelocityServer server; - // This is intentionally made public for plugins like ViaVersion, which inject their own + // These are intentionally made public for plugins like ViaVersion, which inject their own // protocol logic into the proxy. @SuppressWarnings("WeakerAccess") public final ServerChannelInitializerHolder serverChannelInitializer; + @SuppressWarnings("WeakerAccess") + public final BackendChannelInitializerHolder backendChannelInitializer; private final DnsAddressResolverGroup resolverGroup; private final AsyncHttpClient httpClient; @@ -62,6 +64,8 @@ public final class ConnectionManager { this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER); this.serverChannelInitializer = new ServerChannelInitializerHolder( new ServerChannelInitializer(this.server)); + this.backendChannelInitializer = new BackendChannelInitializerHolder( + new BackendChannelInitializer(this.server)); this.resolverGroup = new DnsAddressResolverGroup(new DnsNameResolverBuilder() .channelType(this.transportType.datagramChannelClass) .negativeTtl(15) @@ -204,4 +208,8 @@ public final class ConnectionManager { public AsyncHttpClient getHttpClient() { return httpClient; } + + public BackendChannelInitializerHolder getBackendChannelInitializer() { + return this.backendChannelInitializer; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java index 2ef08dade..87b0097b6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java @@ -185,6 +185,10 @@ public class VelocityEventManager implements EventManager { fireEvent(new ProxyShutdownEvent()); } + public ExecutorService getService() { + return service; + } + private static class VelocityMethodScanner implements MethodScanner { @Override