diff --git a/README.md b/README.md index 11e91540a..018884003 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,15 @@ wrapper script (`./gradlew`) as our CI builds using it. It is sufficient to run `./gradlew build` to run the full build cycle. +## Running + +Once you've built Velocity, you can copy and run the `-all` JAR from +`proxy/build/libs`. Velocity will generate a default configuration file +and you can configure it from there. + +Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads) +page. + ## Status Velocity is far from finished, but most of the essential pieces are in place: diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandExecutor.java b/api/src/main/java/com/velocitypowered/api/command/CommandExecutor.java new file mode 100644 index 000000000..22c6eb765 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/command/CommandExecutor.java @@ -0,0 +1,29 @@ +package com.velocitypowered.api.command; + +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * Represents a command that can be executed by a {@link CommandInvoker}, such as a {@link com.velocitypowered.api.proxy.Player} + * or the console. + */ +public interface CommandExecutor { + /** + * Executes the command for the specified {@link CommandInvoker}. + * @param invoker the invoker of this command + * @param args the arguments for this command + */ + void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args); + + /** + * Provides tab complete suggestions for a command for a specified {@link CommandInvoker}. + * @param invoker the invoker to run the command for + * @param currentArgs the current, partial arguments for this command + * @return tab complete suggestions + */ + default List suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) { + return ImmutableList.of(); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandInvoker.java b/api/src/main/java/com/velocitypowered/api/command/CommandInvoker.java new file mode 100644 index 000000000..22230dcb9 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/command/CommandInvoker.java @@ -0,0 +1,23 @@ +package com.velocitypowered.api.command; + +import net.kyori.text.Component; + +import javax.annotation.Nonnull; + +/** + * Represents something that can be used to run a {@link CommandExecutor}. + */ +public interface CommandInvoker { + /** + * Sends the specified {@code component} to the invoker. + * @param component the text component to send + */ + void sendMessage(@Nonnull Component component); + + /** + * Determines whether or not the invoker has a particular permission. + * @param permission the permission to check for + * @return whether or not the invoker has permission to run this command + */ + boolean hasPermission(@Nonnull String permission); +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java b/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java new file mode 100644 index 000000000..1385b594b --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java @@ -0,0 +1,26 @@ +package com.velocitypowered.api.proxy; + +import java.net.InetSocketAddress; + +/** + * Represents a connection to the proxy. There is no guarantee that the connection has been fully initialized. + */ +public interface InboundConnection { + /** + * Returns the player's IP address. + * @return the player's IP + */ + InetSocketAddress getRemoteAddress(); + + /** + * Determine whether or not the player remains online. + * @return whether or not the player active + */ + boolean isActive(); + + /** + * Returns the current protocol version this connection uses. + * @return the protocol version the connection uses + */ + int getProtocolVersion(); +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index f2f233459..89e4df3a6 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -1,18 +1,18 @@ package com.velocitypowered.api.proxy; +import com.velocitypowered.api.command.CommandInvoker; import com.velocitypowered.api.server.ServerInfo; import com.velocitypowered.api.util.MessagePosition; import net.kyori.text.Component; import javax.annotation.Nonnull; -import java.net.InetSocketAddress; import java.util.Optional; import java.util.UUID; /** * Represents a player who is connected to the proxy. */ -public interface Player { +public interface Player extends CommandInvoker, InboundConnection { /** * Returns the player's current username. * @return the username @@ -31,18 +31,6 @@ public interface Player { */ Optional getCurrentServer(); - /** - * Returns the player's IP address. - * @return the player's IP - */ - InetSocketAddress getRemoteAddress(); - - /** - * Determine whether or not the player remains online. - * @return whether or not the player active - */ - boolean isActive(); - /** * Sends a chat message to the player's client. * @param component the chat message to send diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index 1973482cf..1dbb4b580 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -1,5 +1,6 @@ package com.velocitypowered.api.proxy; +import com.velocitypowered.api.command.CommandInvoker; import com.velocitypowered.api.server.ServerInfo; import javax.annotation.Nonnull; @@ -62,4 +63,12 @@ public interface ProxyServer { * @param server the server to unregister */ void unregisterServer(@Nonnull ServerInfo server); + + /** + * Returns an instance of {@link CommandInvoker} that can be used to determine if the command is being invoked by + * the console or a console-like executor. Plugins that execute commands are strongly urged to implement their own + * {@link CommandInvoker} instead of using the console invoker. + * @return the console command invoker + */ + CommandInvoker getConsoleCommandInvoker(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index abc570553..a6f365b22 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -4,19 +4,24 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.velocitypowered.api.command.CommandInvoker; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.natives.util.Natives; import com.velocitypowered.network.ConnectionManager; +import com.velocitypowered.proxy.command.ServerCommand; +import com.velocitypowered.proxy.command.VelocityCommand; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.http.NettyHttpClient; import com.velocitypowered.api.server.ServerInfo; +import com.velocitypowered.proxy.command.CommandManager; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.EncryptionUtils; import com.velocitypowered.proxy.util.ServerMap; import io.netty.bootstrap.Bootstrap; import net.kyori.text.Component; +import net.kyori.text.serializer.ComponentSerializers; import net.kyori.text.serializer.GsonComponentSerializer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,11 +48,26 @@ public class VelocityServer implements ProxyServer { private NettyHttpClient httpClient; private KeyPair serverKeyPair; private final ServerMap servers = new ServerMap(); + private final CommandManager commandManager = new CommandManager(); private final Map connectionsByUuid = new ConcurrentHashMap<>(); private final Map connectionsByName = new ConcurrentHashMap<>(); + private final CommandInvoker consoleCommandInvoker = new CommandInvoker() { + @Override + public void sendMessage(@Nonnull Component component) { + // TODO: TerminalConsoleAppender + logger.info(ComponentSerializers.PLAIN.serialize(component)); + } + + @Override + public boolean hasPermission(@Nonnull String permission) { + return true; + } + }; private VelocityServer() { + commandManager.registerCommand("velocity", new VelocityCommand()); + commandManager.registerCommand("server", new ServerCommand()); } public static VelocityServer getServer() { @@ -62,6 +82,10 @@ public class VelocityServer implements ProxyServer { return configuration; } + public CommandManager getCommandManager() { + return commandManager; + } + public void start() { logger.info("Using {}", Natives.compressor.getLoadedVariant()); logger.info("Using {}", Natives.cipher.getLoadedVariant()); @@ -177,4 +201,9 @@ public class VelocityServer implements ProxyServer { public void unregisterServer(@Nonnull ServerInfo server) { servers.unregister(server); } + + @Override + public CommandInvoker getConsoleCommandInvoker() { + return consoleCommandInvoker; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java new file mode 100644 index 000000000..5a3753089 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java @@ -0,0 +1,48 @@ +package com.velocitypowered.proxy.command; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.command.CommandExecutor; +import com.velocitypowered.api.command.CommandInvoker; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class CommandManager { + private final Map executors = new HashMap<>(); + + public void registerCommand(String name, CommandExecutor executor) { + Preconditions.checkNotNull(name, "name"); + Preconditions.checkNotNull(executor, "executor"); + this.executors.put(name, executor); + } + + public void unregisterCommand(String name) { + Preconditions.checkNotNull(name, "name"); + this.executors.remove(name); + } + + public boolean execute(CommandInvoker invoker, String cmdLine) { + Preconditions.checkNotNull(invoker, "invoker"); + Preconditions.checkNotNull(cmdLine, "cmdLine"); + + String[] split = cmdLine.split(" ", -1); + if (split.length == 0) { + return false; + } + + String command = split[0]; + String[] actualArgs = Arrays.copyOfRange(split, 1, split.length); + CommandExecutor executor = executors.get(command); + if (executor == null) { + return false; + } + + try { + executor.execute(invoker, actualArgs); + return true; + } catch (Exception e) { + throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + invoker, e); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java new file mode 100644 index 000000000..bb66832ca --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java @@ -0,0 +1,41 @@ +package com.velocitypowered.proxy.command; + +import com.velocitypowered.api.command.CommandExecutor; +import com.velocitypowered.api.command.CommandInvoker; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.server.ServerInfo; +import com.velocitypowered.proxy.VelocityServer; +import net.kyori.text.TextComponent; +import net.kyori.text.format.TextColor; + +import javax.annotation.Nonnull; +import java.util.Optional; +import java.util.stream.Collectors; + +public class ServerCommand implements CommandExecutor { + @Override + public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) { + if (!(invoker instanceof Player)) { + invoker.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED)); + return; + } + + Player player = (Player) invoker; + if (args.length == 1) { + // Trying to connect to a server. + String serverName = args[0]; + Optional server = VelocityServer.getServer().getServerInfo(serverName); + if (!server.isPresent()) { + player.sendMessage(TextComponent.of("Server " + serverName + " doesn't exist.", TextColor.RED)); + return; + } + + player.createConnectionRequest(server.get()).fireAndForget(); + } else { + String serverList = VelocityServer.getServer().getAllServers().stream() + .map(ServerInfo::getName) + .collect(Collectors.joining(", ")); + player.sendMessage(TextComponent.of("Available servers: " + serverList, TextColor.YELLOW)); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java new file mode 100644 index 000000000..20b77e6bc --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java @@ -0,0 +1,42 @@ +package com.velocitypowered.proxy.command; + +import com.velocitypowered.api.command.CommandExecutor; +import com.velocitypowered.api.command.CommandInvoker; +import com.velocitypowered.proxy.VelocityServer; +import net.kyori.text.TextComponent; +import net.kyori.text.event.ClickEvent; +import net.kyori.text.format.TextColor; + +import javax.annotation.Nonnull; + +public class VelocityCommand implements CommandExecutor { + @Override + public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) { + String implVersion = VelocityServer.class.getPackage().getImplementationVersion(); + TextComponent thisIsVelocity = TextComponent.builder() + .content("This is ") + .append(TextComponent.of("Velocity " + implVersion, TextColor.DARK_AQUA)) + .append(TextComponent.of(", the next generation Minecraft: Java Edition proxy.", TextColor.WHITE)) + .build(); + TextComponent velocityInfo = TextComponent.builder() + .content("Copyright 2018 Velocity Contributors. Velocity is freely licensed under the terms of the " + + "MIT License.") + .build(); + TextComponent velocityWebsite = TextComponent.builder() + .content("Visit the ") + .append(TextComponent.builder("Velocity website") + .color(TextColor.GREEN) + .clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.velocitypowered.com")) + .build()) + .append(TextComponent.of(" or the ", TextColor.WHITE)) + .append(TextComponent.builder("Velocity GitHub") + .color(TextColor.GREEN) + .clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/astei/velocity")) + .build()) + .build(); + + invoker.sendMessage(thisIsVelocity); + invoker.sendMessage(velocityInfo); + invoker.sendMessage(velocityWebsite); + } +} 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 bc9c11986..cecc1c80f 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 @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.connection.client; import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.connection.backend.ServerConnection; +import com.velocitypowered.proxy.command.VelocityCommand; import com.velocitypowered.api.server.ServerInfo; import com.velocitypowered.proxy.data.scoreboard.Objective; import com.velocitypowered.proxy.data.scoreboard.Score; @@ -9,16 +9,13 @@ import com.velocitypowered.proxy.data.scoreboard.Scoreboard; import com.velocitypowered.proxy.data.scoreboard.Team; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; -import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.remap.EntityIdRemapper; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.ThrowableUtils; import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; import io.netty.channel.EventLoop; -import io.netty.util.ReferenceCountUtil; import net.kyori.text.TextComponent; import net.kyori.text.format.TextColor; import org.apache.logging.log4j.LogManager; @@ -81,11 +78,21 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { if (packet instanceof Chat) { Chat chat = (Chat) packet; - if (chat.getMessage().equals("/connect")) { - ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25566)); - player.createConnectionRequest(info).fireAndForget(); - return; + String msg = ((Chat) packet).getMessage(); + if (msg.startsWith("/")) { + try { + if (!VelocityServer.getServer().getCommandManager().execute(player, msg.substring(1))) { + player.getConnectedServer().getMinecraftConnection().write(msg); + } + } catch (Exception e) { + logger.info("Exception occurred while running command for {}", player.getProfile().getName(), e); + player.sendMessage(TextComponent.of("An error occurred while running this command.", TextColor.RED)); + return; + } + } else { + player.getConnectedServer().getMinecraftConnection().write(chat); } + return; } if (packet instanceof PluginMessage) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index dda03be7e..a10681fa8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -83,6 +83,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return connection.getChannel().isActive(); } + @Override + public int getProtocolVersion() { + return connection.getProtocolVersion(); + } + @Override public void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position) { Preconditions.checkNotNull(component, "component"); @@ -217,6 +222,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return "[connected player] " + getProfile().getName() + " (" + getRemoteAddress() + ")"; } + @Override + public boolean hasPermission(@Nonnull String permission) { + return false; // TODO: Implement permissions. + } + private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { private final ServerInfo info; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index e16915f6d..700709867 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -1,6 +1,7 @@ package com.velocitypowered.proxy.connection.client; import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -14,6 +15,8 @@ import net.kyori.text.TextComponent; import net.kyori.text.TranslatableComponent; import net.kyori.text.format.TextColor; +import java.net.InetSocketAddress; + public class HandshakeSessionHandler implements MinecraftSessionHandler { private final MinecraftConnection connection; @@ -70,4 +73,27 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED))); } } + + private static class InitialInboundConnection implements InboundConnection { + private final MinecraftConnection connection; + + private InitialInboundConnection(MinecraftConnection connection) { + this.connection = connection; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return (InetSocketAddress) connection.getChannel().remoteAddress(); + } + + @Override + public boolean isActive() { + return connection.getChannel().isActive(); + } + + @Override + public int getProtocolVersion() { + return connection.getProtocolVersion(); + } + } }