Reformat with Google code style and enforce Checkstyle.

Fixes #125
This commit is contained in:
Andrew Steinborn
2018-10-27 23:45:35 -04:00
parent 53aa92db92
commit 25b5e00125
208 changed files with 12851 additions and 11620 deletions

View File

@@ -1,29 +1,29 @@
package com.velocitypowered.proxy;
import java.text.DecimalFormat;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.text.DecimalFormat;
public class Velocity {
private static final Logger logger = LogManager.getLogger(Velocity.class);
static {
// We use BufferedImage for favicons, and on macOS this puts the Java application in the dock. How inconvenient.
// Force AWT to work with its head chopped off.
System.setProperty("java.awt.headless", "true");
}
private static final Logger logger = LogManager.getLogger(Velocity.class);
public static void main(String... args) {
long startTime = System.currentTimeMillis();
static {
// We use BufferedImage for favicons, and on macOS this puts the Java application in the dock. How inconvenient.
// Force AWT to work with its head chopped off.
System.setProperty("java.awt.headless", "true");
}
VelocityServer server = new VelocityServer();
server.start();
public static void main(String... args) {
long startTime = System.currentTimeMillis();
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown, "Shutdown thread"));
VelocityServer server = new VelocityServer();
server.start();
double bootTime = (System.currentTimeMillis() - startTime) / 1000d;
logger.info("Done ({}s)!", new DecimalFormat("#.##").format(bootTime));
server.getConsoleCommandSource().start();
}
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown, "Shutdown thread"));
double bootTime = (System.currentTimeMillis() - startTime) / 1000d;
logger.info("Done ({}s)!", new DecimalFormat("#.##").format(bootTime));
server.getConsoleCommandSource().start();
}
}

View File

@@ -24,7 +24,6 @@ import com.velocitypowered.proxy.config.AnnotatedConfig;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.console.VelocityConsole;
import com.velocitypowered.proxy.util.VelocityChannelRegistrar;
import com.velocitypowered.proxy.network.ConnectionManager;
import com.velocitypowered.proxy.network.http.NettyHttpClient;
import com.velocitypowered.proxy.plugin.VelocityEventManager;
@@ -36,356 +35,367 @@ import com.velocitypowered.proxy.server.ServerMap;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.Ratelimiter;
import com.velocitypowered.proxy.util.VelocityChannelRegistrar;
import io.netty.bootstrap.Bootstrap;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.serializer.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.*;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.*;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.serializer.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public class VelocityServer implements ProxyServer {
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
public static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
.registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer())
.create();
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
public static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
.registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer())
.create();
private @MonotonicNonNull ConnectionManager cm;
private @MonotonicNonNull VelocityConfiguration configuration;
private @MonotonicNonNull NettyHttpClient httpClient;
private @MonotonicNonNull KeyPair serverKeyPair;
private @MonotonicNonNull ServerMap servers;
private final VelocityCommandManager commandManager = new VelocityCommandManager();
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
private boolean shutdown = false;
private @MonotonicNonNull VelocityPluginManager pluginManager;
private @MonotonicNonNull ConnectionManager cm;
private @MonotonicNonNull VelocityConfiguration configuration;
private @MonotonicNonNull NettyHttpClient httpClient;
private @MonotonicNonNull KeyPair serverKeyPair;
private @MonotonicNonNull ServerMap servers;
private final VelocityCommandManager commandManager = new VelocityCommandManager();
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
private boolean shutdown = false;
private @MonotonicNonNull VelocityPluginManager pluginManager;
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private @MonotonicNonNull VelocityConsole console;
private @MonotonicNonNull Ratelimiter ipAttemptLimiter;
private @MonotonicNonNull VelocityEventManager eventManager;
private @MonotonicNonNull VelocityScheduler scheduler;
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private @MonotonicNonNull VelocityConsole console;
private @MonotonicNonNull Ratelimiter ipAttemptLimiter;
private @MonotonicNonNull VelocityEventManager eventManager;
private @MonotonicNonNull VelocityScheduler scheduler;
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
public KeyPair getServerKeyPair() {
if (serverKeyPair == null) {
throw new AssertionError();
}
return serverKeyPair;
public KeyPair getServerKeyPair() {
if (serverKeyPair == null) {
throw new AssertionError();
}
return serverKeyPair;
}
public VelocityConfiguration getConfiguration() {
VelocityConfiguration cfg = this.configuration;
if (cfg == null) {
throw new IllegalStateException("Configuration not initialized!");
}
return cfg;
}
@Override
public ProxyVersion getVersion() {
Package pkg = VelocityServer.class.getPackage();
String implName;
String implVersion;
String implVendor;
if (pkg != null) {
implName = MoreObjects.firstNonNull(pkg.getImplementationTitle(), "Velocity");
implVersion = MoreObjects.firstNonNull(pkg.getImplementationVersion(), "<unknown>");
implVendor = MoreObjects.firstNonNull(pkg.getImplementationVendor(), "Velocity Contributors");
} else {
implName = "Velocity";
implVersion = "<unknown>";
implVendor = "Velocity Contributors";
}
public VelocityConfiguration getConfiguration() {
VelocityConfiguration cfg = this.configuration;
if (cfg == null) {
throw new IllegalStateException("Configuration not initialized!");
}
return cfg;
return new ProxyVersion(implName, implVendor, implVersion);
}
@Override
public VelocityCommandManager getCommandManager() {
return commandManager;
}
@EnsuresNonNull({"serverKeyPair", "servers", "pluginManager", "eventManager", "scheduler",
"console", "cm", "configuration"})
public void start() {
logger.info("Booting up {} {}...", getVersion().getName(), getVersion().getVersion());
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
pluginManager = new VelocityPluginManager(this);
eventManager = new VelocityEventManager(pluginManager);
scheduler = new VelocityScheduler(pluginManager);
console = new VelocityConsole(this);
cm = new ConnectionManager(this);
servers = new ServerMap(this);
cm.logChannelInformation();
// Initialize commands first
commandManager.register(new VelocityCommand(this), "velocity");
commandManager.register(new ServerCommand(this), "server");
commandManager.register(new ShutdownCommand(this), "shutdown", "end");
try {
Path configPath = Paths.get("velocity.toml");
configuration = VelocityConfiguration.read(configPath);
if (!configuration.validate()) {
logger.error(
"Your configuration is invalid. Velocity will refuse to start up until the errors are resolved.");
LogManager.shutdown();
System.exit(1);
}
AnnotatedConfig
.saveConfig(configuration.dumpConfig(), configPath); //Resave config to add new values
} catch (Exception e) {
logger.error("Unable to read/load/save your velocity.toml. The server will shut down.", e);
LogManager.shutdown();
System.exit(1);
}
@Override
public ProxyVersion getVersion() {
Package pkg = VelocityServer.class.getPackage();
String implName;
String implVersion;
String implVendor;
if (pkg != null) {
implName = MoreObjects.firstNonNull(pkg.getImplementationTitle(), "Velocity");
implVersion = MoreObjects.firstNonNull(pkg.getImplementationVersion(), "<unknown>");
implVendor = MoreObjects.firstNonNull(pkg.getImplementationVendor(), "Velocity Contributors");
} else {
implName = "Velocity";
implVersion = "<unknown>";
implVendor = "Velocity Contributors";
for (Map.Entry<String, String> entry : configuration.getServers().entrySet()) {
servers.register(new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue())));
}
ipAttemptLimiter = new Ratelimiter(configuration.getLoginRatelimit());
httpClient = new NettyHttpClient(this);
loadPlugins();
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
// to fully initialize before we accept any connections to the server.
eventManager.fire(new ProxyInitializeEvent()).join();
// init console permissions after plugins are loaded
console.setupPermissions();
this.cm.bind(configuration.getBind());
if (configuration.isQueryEnabled()) {
this.cm.queryBind(configuration.getBind().getHostString(), configuration.getQueryPort());
}
}
@RequiresNonNull({"pluginManager", "eventManager"})
private void loadPlugins() {
logger.info("Loading plugins...");
try {
Path pluginPath = Paths.get("plugins");
if (!pluginPath.toFile().exists()) {
Files.createDirectory(pluginPath);
} else {
if (!pluginPath.toFile().isDirectory()) {
logger.warn("Plugin location {} is not a directory, continuing without loading plugins",
pluginPath);
return;
}
return new ProxyVersion(implName, implVendor, implVersion);
pluginManager.loadPlugins(pluginPath);
}
} catch (Exception e) {
logger.error("Couldn't load plugins", e);
}
@Override
public VelocityCommandManager getCommandManager() {
return commandManager;
// Register the plugin main classes so that we may proceed with firing the proxy initialize event
for (PluginContainer plugin : pluginManager.getPlugins()) {
Optional<?> instance = plugin.getInstance();
if (instance.isPresent()) {
eventManager.register(instance.get(), instance.get());
}
}
@EnsuresNonNull({"serverKeyPair", "servers", "pluginManager", "eventManager", "scheduler", "console", "cm", "configuration"})
public void start() {
logger.info("Booting up {} {}...", getVersion().getName(), getVersion().getVersion());
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
}
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
pluginManager = new VelocityPluginManager(this);
eventManager = new VelocityEventManager(pluginManager);
scheduler = new VelocityScheduler(pluginManager);
console = new VelocityConsole(this);
cm = new ConnectionManager(this);
servers = new ServerMap(this);
public Bootstrap initializeGenericBootstrap() {
if (cm == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return this.cm.createWorker();
}
cm.logChannelInformation();
public boolean isShutdown() {
return shutdown;
}
// Initialize commands first
commandManager.register(new VelocityCommand(this), "velocity");
commandManager.register(new ServerCommand(this), "server");
commandManager.register(new ShutdownCommand(this), "shutdown", "end");
try {
Path configPath = Paths.get("velocity.toml");
configuration = VelocityConfiguration.read(configPath);
if (!configuration.validate()) {
logger.error("Your configuration is invalid. Velocity will refuse to start up until the errors are resolved.");
LogManager.shutdown();
System.exit(1);
}
AnnotatedConfig.saveConfig(configuration.dumpConfig(), configPath); //Resave config to add new values
} catch (Exception e) {
logger.error("Unable to read/load/save your velocity.toml. The server will shut down.", e);
LogManager.shutdown();
System.exit(1);
}
for (Map.Entry<String, String> entry : configuration.getServers().entrySet()) {
servers.register(new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue())));
}
ipAttemptLimiter = new Ratelimiter(configuration.getLoginRatelimit());
httpClient = new NettyHttpClient(this);
loadPlugins();
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
// to fully initialize before we accept any connections to the server.
eventManager.fire(new ProxyInitializeEvent()).join();
// init console permissions after plugins are loaded
console.setupPermissions();
this.cm.bind(configuration.getBind());
if (configuration.isQueryEnabled()) {
this.cm.queryBind(configuration.getBind().getHostString(), configuration.getQueryPort());
}
public void shutdown() {
if (eventManager == null || pluginManager == null || cm == null || scheduler == null) {
throw new AssertionError();
}
@RequiresNonNull({"pluginManager", "eventManager"})
private void loadPlugins() {
logger.info("Loading plugins...");
if (!shutdownInProgress.compareAndSet(false, true)) {
return;
}
logger.info("Shutting down the proxy...");
try {
Path pluginPath = Paths.get("plugins");
if (!pluginPath.toFile().exists()) {
Files.createDirectory(pluginPath);
} else {
if (!pluginPath.toFile().isDirectory()) {
logger.warn("Plugin location {} is not a directory, continuing without loading plugins", pluginPath);
return;
}
pluginManager.loadPlugins(pluginPath);
}
} catch (Exception e) {
logger.error("Couldn't load plugins", e);
}
// Register the plugin main classes so that we may proceed with firing the proxy initialize event
for (PluginContainer plugin : pluginManager.getPlugins()) {
Optional<?> instance = plugin.getInstance();
if (instance.isPresent()) {
eventManager.register(instance.get(), instance.get());
}
}
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
for (ConnectedPlayer player : ImmutableList.copyOf(connectionsByUuid.values())) {
player.close(TextComponent.of("Proxy shutting down."));
}
public Bootstrap initializeGenericBootstrap() {
if (cm == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return this.cm.createWorker();
this.cm.shutdown();
eventManager.fire(new ProxyShutdownEvent());
try {
if (!eventManager.shutdown() || !scheduler.shutdown()) {
logger.error("Your plugins took over 10 seconds to shut down.");
}
} catch (InterruptedException e) {
// Not much we can do about this...
Thread.currentThread().interrupt();
}
public boolean isShutdown() {
return shutdown;
shutdown = true;
}
public NettyHttpClient getHttpClient() {
if (httpClient == null) {
throw new IllegalStateException("HTTP client not initialized");
}
return httpClient;
}
public void shutdown() {
if (eventManager == null || pluginManager == null || cm == null || scheduler == null) {
throw new AssertionError();
}
if (!shutdownInProgress.compareAndSet(false, true)) {
return;
}
logger.info("Shutting down the proxy...");
for (ConnectedPlayer player : ImmutableList.copyOf(connectionsByUuid.values())) {
player.close(TextComponent.of("Proxy shutting down."));
}
this.cm.shutdown();
eventManager.fire(new ProxyShutdownEvent());
try {
if (!eventManager.shutdown() || !scheduler.shutdown()) {
logger.error("Your plugins took over 10 seconds to shut down.");
}
} catch (InterruptedException e) {
// Not much we can do about this...
Thread.currentThread().interrupt();
}
shutdown = true;
public Ratelimiter getIpAttemptLimiter() {
if (ipAttemptLimiter == null) {
throw new IllegalStateException("Ratelimiter not initialized");
}
return ipAttemptLimiter;
}
public NettyHttpClient getHttpClient() {
if (httpClient == null) {
throw new IllegalStateException("HTTP client not initialized");
}
return httpClient;
public boolean registerConnection(ConnectedPlayer connection) {
String lowerName = connection.getUsername().toLowerCase(Locale.US);
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
return false;
}
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
connectionsByName.remove(lowerName, connection);
return false;
}
return true;
}
public Ratelimiter getIpAttemptLimiter() {
if (ipAttemptLimiter == null) {
throw new IllegalStateException("Ratelimiter not initialized");
}
return ipAttemptLimiter;
}
public void unregisterConnection(ConnectedPlayer connection) {
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
connectionsByUuid.remove(connection.getUniqueId(), connection);
}
public boolean registerConnection(ConnectedPlayer connection) {
String lowerName = connection.getUsername().toLowerCase(Locale.US);
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
return false;
}
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
connectionsByName.remove(lowerName, connection);
return false;
}
return true;
}
@Override
public Optional<Player> getPlayer(String username) {
Preconditions.checkNotNull(username, "username");
return Optional.ofNullable(connectionsByName.get(username.toLowerCase(Locale.US)));
}
public void unregisterConnection(ConnectedPlayer connection) {
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
connectionsByUuid.remove(connection.getUniqueId(), connection);
}
@Override
public Optional<Player> getPlayer(UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid");
return Optional.ofNullable(connectionsByUuid.get(uuid));
}
@Override
public Optional<Player> getPlayer(String username) {
Preconditions.checkNotNull(username, "username");
return Optional.ofNullable(connectionsByName.get(username.toLowerCase(Locale.US)));
@Override
public void broadcast(Component component) {
Preconditions.checkNotNull(component, "component");
Chat chat = Chat.createClientbound(component);
for (ConnectedPlayer player : connectionsByUuid.values()) {
player.getConnection().write(chat);
}
}
@Override
public Optional<Player> getPlayer(UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid");
return Optional.ofNullable(connectionsByUuid.get(uuid));
}
@Override
public Collection<Player> getAllPlayers() {
return ImmutableList.copyOf(connectionsByUuid.values());
}
@Override
public void broadcast(Component component) {
Preconditions.checkNotNull(component, "component");
Chat chat = Chat.createClientbound(component);
for (ConnectedPlayer player : connectionsByUuid.values()) {
player.getConnection().write(chat);
}
}
@Override
public int getPlayerCount() {
return connectionsByUuid.size();
}
@Override
public Collection<Player> getAllPlayers() {
return ImmutableList.copyOf(connectionsByUuid.values());
@Override
public Optional<RegisteredServer> getServer(String name) {
Preconditions.checkNotNull(name, "name");
if (servers == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return servers.getServer(name);
}
@Override
public int getPlayerCount() {
return connectionsByUuid.size();
@Override
public Collection<RegisteredServer> getAllServers() {
if (servers == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return servers.getAllServers();
}
@Override
public Optional<RegisteredServer> getServer(String name) {
Preconditions.checkNotNull(name, "name");
if (servers == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return servers.getServer(name);
@Override
public RegisteredServer registerServer(ServerInfo server) {
if (servers == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return servers.register(server);
}
@Override
public Collection<RegisteredServer> getAllServers() {
if (servers == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return servers.getAllServers();
@Override
public void unregisterServer(ServerInfo server) {
if (servers == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
servers.unregister(server);
}
@Override
public RegisteredServer registerServer(ServerInfo server) {
if (servers == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return servers.register(server);
@Override
public VelocityConsole getConsoleCommandSource() {
if (console == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return console;
}
@Override
public void unregisterServer(ServerInfo server) {
if (servers == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
servers.unregister(server);
@Override
public PluginManager getPluginManager() {
if (pluginManager == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return pluginManager;
}
@Override
public VelocityConsole getConsoleCommandSource() {
if (console == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return console;
@Override
public EventManager getEventManager() {
if (eventManager == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return eventManager;
}
@Override
public PluginManager getPluginManager() {
if (pluginManager == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return pluginManager;
@Override
public VelocityScheduler getScheduler() {
if (scheduler == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return scheduler;
}
@Override
public EventManager getEventManager() {
if (eventManager == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return eventManager;
}
@Override
public VelocityChannelRegistrar getChannelRegistrar() {
return channelRegistrar;
}
@Override
public VelocityScheduler getScheduler() {
if (scheduler == null) {
throw new IllegalStateException("Server did not initialize properly.");
}
return scheduler;
}
@Override
public VelocityChannelRegistrar getChannelRegistrar() {
return channelRegistrar;
}
@Override
public InetSocketAddress getBoundAddress() {
if (configuration == null) {
throw new IllegalStateException("No configuration"); // even though you'll never get the chance... heh, heh
}
return configuration.getBind();
@Override
public InetSocketAddress getBoundAddress() {
if (configuration == null) {
throw new IllegalStateException(
"No configuration"); // even though you'll never get the chance... heh, heh
}
return configuration.getBind();
}
}

View File

@@ -9,90 +9,96 @@ 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.List;
import java.util.Optional;
import java.util.stream.Collectors;
import net.kyori.text.TextComponent;
import net.kyori.text.event.ClickEvent;
import net.kyori.text.event.HoverEvent;
import net.kyori.text.format.TextColor;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class ServerCommand implements Command {
private final ProxyServer server;
public ServerCommand(ProxyServer server) {
this.server = server;
private final ProxyServer server;
public ServerCommand(ProxyServer server) {
this.server = server;
}
@Override
public void execute(CommandSource source, String @NonNull [] args) {
if (!(source instanceof Player)) {
source.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
return;
}
@Override
public void execute(CommandSource source, String @NonNull [] args) {
if (!(source instanceof Player)) {
source.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
return;
}
Player player = (Player) source;
if (args.length == 1) {
// Trying to connect to a server.
String serverName = args[0];
Optional<RegisteredServer> toConnect = server.getServer(serverName);
if (!toConnect.isPresent()) {
player.sendMessage(
TextComponent.of("Server " + serverName + " doesn't exist.", TextColor.RED));
return;
}
Player player = (Player) source;
if (args.length == 1) {
// Trying to connect to a server.
String serverName = args[0];
Optional<RegisteredServer> toConnect = server.getServer(serverName);
if (!toConnect.isPresent()) {
player.sendMessage(TextComponent.of("Server " + serverName + " doesn't exist.", TextColor.RED));
return;
}
player.createConnectionRequest(toConnect.get()).fireAndForget();
} else {
String currentServer = player.getCurrentServer().map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("<unknown>");
player.sendMessage(TextComponent
.of("You are currently connected to " + currentServer + ".", TextColor.YELLOW));
player.createConnectionRequest(toConnect.get()).fireAndForget();
// Assemble the list of servers as components
TextComponent.Builder serverListBuilder = TextComponent.builder("Available servers: ")
.color(TextColor.YELLOW);
List<RegisteredServer> infos = ImmutableList.copyOf(server.getAllServers());
for (int i = 0; i < infos.size(); i++) {
RegisteredServer rs = infos.get(i);
TextComponent infoComponent = TextComponent.of(rs.getServerInfo().getName());
String playersText = rs.getPlayersConnected().size() + " player(s) online";
if (rs.getServerInfo().getName().equals(currentServer)) {
infoComponent = infoComponent.color(TextColor.GREEN)
.hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
TextComponent.of("Currently connected to this server\n" + playersText)));
} else {
String currentServer = player.getCurrentServer().map(ServerConnection::getServerInfo).map(ServerInfo::getName)
.orElse("<unknown>");
player.sendMessage(TextComponent.of("You are currently connected to " + currentServer + ".", TextColor.YELLOW));
// Assemble the list of servers as components
TextComponent.Builder serverListBuilder = TextComponent.builder("Available servers: ").color(TextColor.YELLOW);
List<RegisteredServer> infos = ImmutableList.copyOf(server.getAllServers());
for (int i = 0; i < infos.size(); i++) {
RegisteredServer rs = infos.get(i);
TextComponent infoComponent = TextComponent.of(rs.getServerInfo().getName());
String playersText = rs.getPlayersConnected().size() + " player(s) online";
if (rs.getServerInfo().getName().equals(currentServer)) {
infoComponent = infoComponent.color(TextColor.GREEN)
.hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
TextComponent.of("Currently connected to this server\n" + playersText)));
} else {
infoComponent = infoComponent.color(TextColor.GRAY)
.clickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/server " + rs.getServerInfo().getName()))
.hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Click to connect to this server\n" + playersText)));
}
serverListBuilder.append(infoComponent);
if (i != infos.size() - 1) {
serverListBuilder.append(TextComponent.of(", ", TextColor.GRAY));
}
}
player.sendMessage(serverListBuilder.build());
infoComponent = infoComponent.color(TextColor.GRAY)
.clickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
"/server " + rs.getServerInfo().getName()))
.hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
TextComponent.of("Click to connect to this server\n" + playersText)));
}
}
@Override
public List<String> suggest(CommandSource source, String @NonNull [] currentArgs) {
if (currentArgs.length == 0) {
return server.getAllServers().stream()
.map(rs -> rs.getServerInfo().getName())
.collect(Collectors.toList());
} else if (currentArgs.length == 1) {
return server.getAllServers().stream()
.map(rs -> rs.getServerInfo().getName())
.filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length()))
.collect(Collectors.toList());
} else {
return ImmutableList.of();
serverListBuilder.append(infoComponent);
if (i != infos.size() - 1) {
serverListBuilder.append(TextComponent.of(", ", TextColor.GRAY));
}
}
}
@Override
public boolean hasPermission(CommandSource source, String @NonNull [] args) {
return source.getPermissionValue("velocity.command.server") != Tristate.FALSE;
player.sendMessage(serverListBuilder.build());
}
}
@Override
public List<String> suggest(CommandSource source, String @NonNull [] currentArgs) {
if (currentArgs.length == 0) {
return server.getAllServers().stream()
.map(rs -> rs.getServerInfo().getName())
.collect(Collectors.toList());
} else if (currentArgs.length == 1) {
return server.getAllServers().stream()
.map(rs -> rs.getServerInfo().getName())
.filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length()))
.collect(Collectors.toList());
} else {
return ImmutableList.of();
}
}
@Override
public boolean hasPermission(CommandSource source, String @NonNull [] args) {
return source.getPermissionValue("velocity.command.server") != Tristate.FALSE;
}
}

View File

@@ -8,23 +8,25 @@ import net.kyori.text.format.TextColor;
import org.checkerframework.checker.nullness.qual.NonNull;
public class ShutdownCommand implements Command {
private final VelocityServer server;
public ShutdownCommand(VelocityServer server) {
this.server = server;
}
private final VelocityServer server;
@Override
public void execute(CommandSource source, String @NonNull [] args) {
if (source != server.getConsoleCommandSource()) {
source.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED));
return;
}
server.shutdown();
}
public ShutdownCommand(VelocityServer server) {
this.server = server;
}
@Override
public boolean hasPermission(CommandSource source, String @NonNull [] args) {
return source == server.getConsoleCommandSource();
@Override
public void execute(CommandSource source, String @NonNull [] args) {
if (source != server.getConsoleCommandSource()) {
source
.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED));
return;
}
server.shutdown();
}
@Override
public boolean hasPermission(CommandSource source, String @NonNull [] args) {
return source == server.getConsoleCommandSource();
}
}

View File

@@ -7,125 +7,130 @@ import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.permission.Tristate;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.util.ProxyVersion;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import net.kyori.text.TextComponent;
import net.kyori.text.event.ClickEvent;
import net.kyori.text.format.TextColor;
import net.kyori.text.format.TextDecoration;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
public class VelocityCommand implements Command {
private final Map<String, Command> subcommands;
public VelocityCommand(ProxyServer server) {
this.subcommands = ImmutableMap.<String, Command>builder()
.put("version", new Info(server))
.build();
private final Map<String, Command> subcommands;
public VelocityCommand(ProxyServer server) {
this.subcommands = ImmutableMap.<String, Command>builder()
.put("version", new Info(server))
.build();
}
private void usage(CommandSource source) {
String commandText = "/velocity <" + String.join("|", subcommands.keySet()) + ">";
source.sendMessage(TextComponent.of(commandText, TextColor.RED));
}
@Override
public void execute(CommandSource source, String @NonNull [] args) {
if (args.length == 0) {
usage(source);
return;
}
private void usage(CommandSource source) {
String commandText = "/velocity <" + String.join("|", subcommands.keySet()) + ">";
source.sendMessage(TextComponent.of(commandText, TextColor.RED));
Command command = subcommands.get(args[0].toLowerCase(Locale.US));
if (command == null) {
usage(source);
return;
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(args, 1, args.length);
command.execute(source, actualArgs);
}
@Override
public List<String> suggest(CommandSource source, String @NonNull [] currentArgs) {
if (currentArgs.length == 0) {
return ImmutableList.copyOf(subcommands.keySet());
}
if (currentArgs.length == 1) {
return subcommands.keySet().stream()
.filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length()))
.collect(Collectors.toList());
}
Command command = subcommands.get(currentArgs[0].toLowerCase(Locale.US));
if (command == null) {
return ImmutableList.of();
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(currentArgs, 1, currentArgs.length);
return command.suggest(source, actualArgs);
}
@Override
public boolean hasPermission(CommandSource source, String @NonNull [] args) {
if (args.length == 0) {
return true;
}
Command command = subcommands.get(args[0].toLowerCase(Locale.US));
if (command == null) {
return true;
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(args, 1, args.length);
return command.hasPermission(source, actualArgs);
}
private static class Info implements Command {
private final ProxyServer server;
private Info(ProxyServer server) {
this.server = server;
}
@Override
public void execute(CommandSource source, String @NonNull [] args) {
if (args.length == 0) {
usage(source);
return;
}
ProxyVersion version = server.getVersion();
Command command = subcommands.get(args[0].toLowerCase(Locale.US));
if (command == null) {
usage(source);
return;
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(args, 1, args.length);
command.execute(source, actualArgs);
}
TextComponent velocity = TextComponent.builder(version.getName() + " ")
.decoration(TextDecoration.BOLD, true)
.color(TextColor.DARK_AQUA)
.append(TextComponent.of(version.getVersion()).decoration(TextDecoration.BOLD, false))
.build();
TextComponent copyright = TextComponent
.of("Copyright 2018 " + version.getVendor() + ". " + version.getName()
+ " is freely licensed under the terms of the " +
"MIT License.");
source.sendMessage(velocity);
source.sendMessage(copyright);
@Override
public List<String> suggest(CommandSource source, String @NonNull [] currentArgs) {
if (currentArgs.length == 0) {
return ImmutableList.copyOf(subcommands.keySet());
}
if (currentArgs.length == 1) {
return subcommands.keySet().stream()
.filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length()))
.collect(Collectors.toList());
}
Command command = subcommands.get(currentArgs[0].toLowerCase(Locale.US));
if (command == null) {
return ImmutableList.of();
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(currentArgs, 1, currentArgs.length);
return command.suggest(source, actualArgs);
if (version.getName().equals("Velocity")) {
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 ").resetStyle())
.append(TextComponent.builder("Velocity GitHub")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL,
"https://github.com/VelocityPowered/Velocity"))
.build())
.build();
source.sendMessage(velocityWebsite);
}
}
@Override
public boolean hasPermission(CommandSource source, String @NonNull [] args) {
if (args.length == 0) {
return true;
}
Command command = subcommands.get(args[0].toLowerCase(Locale.US));
if (command == null) {
return true;
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(args, 1, args.length);
return command.hasPermission(source, actualArgs);
}
private static class Info implements Command {
private final ProxyServer server;
private Info(ProxyServer server) {
this.server = server;
}
@Override
public void execute(CommandSource source, String @NonNull [] args) {
ProxyVersion version = server.getVersion();
TextComponent velocity = TextComponent.builder(version.getName() + " ")
.decoration(TextDecoration.BOLD, true)
.color(TextColor.DARK_AQUA)
.append(TextComponent.of(version.getVersion()).decoration(TextDecoration.BOLD, false))
.build();
TextComponent copyright = TextComponent.of("Copyright 2018 " + version.getVendor() + ". " + version.getName() + " is freely licensed under the terms of the " +
"MIT License.");
source.sendMessage(velocity);
source.sendMessage(copyright);
if (version.getName().equals("Velocity")) {
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 ").resetStyle())
.append(TextComponent.builder("Velocity GitHub")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/VelocityPowered/Velocity"))
.build())
.build();
source.sendMessage(velocityWebsite);
}
}
@Override
public boolean hasPermission(CommandSource source, String @NonNull [] args) {
return source.getPermissionValue("velocity.command.info") != Tristate.FALSE;
}
return source.getPermissionValue("velocity.command.info") != Tristate.FALSE;
}
}
}

View File

@@ -5,100 +5,106 @@ import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.*;
public class VelocityCommandManager implements CommandManager {
private final Map<String, Command> commands = new HashMap<>();
@Override
public void register(@NonNull final Command command, final String... aliases) {
Preconditions.checkNotNull(aliases, "aliases");
Preconditions.checkNotNull(command, "executor");
for (int i = 0, length = aliases.length; i < length; i++) {
final String alias = aliases[i];
Preconditions.checkNotNull(aliases, "alias at index %s", i);
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
}
private final Map<String, Command> commands = new HashMap<>();
@Override
public void register(@NonNull final Command command, final String... aliases) {
Preconditions.checkNotNull(aliases, "aliases");
Preconditions.checkNotNull(command, "executor");
for (int i = 0, length = aliases.length; i < length; i++) {
final String alias = aliases[i];
Preconditions.checkNotNull(aliases, "alias at index %s", i);
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
}
}
@Override
public void unregister(@NonNull final String alias) {
Preconditions.checkNotNull(alias, "name");
this.commands.remove(alias.toLowerCase(Locale.ENGLISH));
}
@Override
public boolean execute(@NonNull CommandSource source, @NonNull String cmdLine) {
Preconditions.checkNotNull(source, "invoker");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return false;
}
@Override
public void unregister(@NonNull final String alias) {
Preconditions.checkNotNull(alias, "name");
this.commands.remove(alias.toLowerCase(Locale.ENGLISH));
String alias = split[0];
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
return false;
}
@Override
public boolean execute(@NonNull CommandSource source, @NonNull String cmdLine) {
Preconditions.checkNotNull(source, "invoker");
Preconditions.checkNotNull(cmdLine, "cmdLine");
try {
if (!command.hasPermission(source, actualArgs)) {
return false;
}
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return false;
}
command.execute(source, actualArgs);
return true;
} catch (Exception e) {
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
}
}
String alias = split[0];
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
return false;
}
public boolean hasCommand(String command) {
return commands.containsKey(command);
}
try {
if (!command.hasPermission(source, actualArgs)) {
return false;
}
public List<String> offerSuggestions(CommandSource source, String cmdLine) {
Preconditions.checkNotNull(source, "source");
Preconditions.checkNotNull(cmdLine, "cmdLine");
command.execute(source, actualArgs);
return true;
} catch (Exception e) {
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
}
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return ImmutableList.of();
}
public boolean hasCommand(String command) {
return commands.containsKey(command);
String alias = split[0];
if (split.length == 1) {
List<String> availableCommands = new ArrayList<>();
for (Map.Entry<String, Command> entry : commands.entrySet()) {
if (entry.getKey().regionMatches(true, 0, alias, 0, alias.length()) &&
entry.getValue().hasPermission(source, new String[0])) {
availableCommands.add("/" + entry.getKey());
}
}
return availableCommands;
}
public List<String> offerSuggestions(CommandSource source, String cmdLine) {
Preconditions.checkNotNull(source, "source");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return ImmutableList.of();
}
String alias = split[0];
if (split.length == 1) {
List<String> availableCommands = new ArrayList<>();
for (Map.Entry<String, Command> entry : commands.entrySet()) {
if (entry.getKey().regionMatches(true, 0, alias, 0, alias.length()) &&
entry.getValue().hasPermission(source, new String[0])) {
availableCommands.add("/" + entry.getKey());
}
}
return availableCommands;
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
return ImmutableList.of();
}
try {
if (!command.hasPermission(source, actualArgs)) {
return ImmutableList.of();
}
return command.suggest(source, actualArgs);
} catch (Exception e) {
throw new RuntimeException("Unable to invoke suggestions for command " + alias + " for " + source, e);
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
return ImmutableList.of();
}
try {
if (!command.hasPermission(source, actualArgs)) {
return ImmutableList.of();
}
return command.suggest(source, actualArgs);
} catch (Exception e) {
throw new RuntimeException(
"Unable to invoke suggestions for command " + alias + " for " + source, e);
}
}
}

View File

@@ -1,8 +1,5 @@
package com.velocitypowered.proxy.config;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -19,206 +16,221 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Simple annotation and fields based TOML configuration serializer
*/
public abstract class AnnotatedConfig {
private static final Logger logger = LogManager.getLogger(AnnotatedConfig.class);
public static Logger getLogger() {
return logger;
}
private static final Logger logger = LogManager.getLogger(AnnotatedConfig.class);
/**
* Indicates that a field is a table
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Table {
String value();
}
public static Logger getLogger() {
return logger;
}
/**
* Creates a comment
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Comment {
String[] value();
}
/**
* Indicates that a field is a table
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Table {
/**
* How field will be named in config
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface ConfigKey {
String value();
}
String value();
}
/**
* Indicates that a field is a map and we need to save all map data to
* config
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface IsMap {}
/**
* Creates a comment
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Comment {
/**
* Indicates that a field is a string converted to byte[]
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface StringAsBytes {}
String[] value();
}
/**
* Indicates that a field should be skipped
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Ignore {}
/**
* How field will be named in config
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface ConfigKey {
/**
* Dumps this configuration to list of strings using {@link #dumpConfig(Object)}
* @return configuration dump
*/
public List<String> dumpConfig() {
return dumpConfig(this);
}
String value();
}
/**
* Creates TOML configuration from supplied <pre>dumpable</pre> object.
*
* @param dumpable object which is going to be dumped
* @throws RuntimeException if reading field value(s) fail
* @return string list of configuration file lines
*/
private static List<String> dumpConfig(Object dumpable) {
List<String> lines = new ArrayList<>();
try {
for (Field field : dumpable.getClass().getDeclaredFields()) {
// Skip fields with @Ignore annotation
if (field.getAnnotation(Ignore.class) != null) {
continue;
}
/**
* Indicates that a field is a map and we need to save all map data to config
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface IsMap {
// Make field accessible
field.setAccessible(true);
}
// Add comments
Comment comment = field.getAnnotation(Comment.class);
if (comment != null) {
for (String line : comment.value()) {
lines.add("# " + line);
}
}
/**
* Indicates that a field is a string converted to byte[]
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface StringAsBytes {
// Get a key name for config
ConfigKey key = field.getAnnotation(ConfigKey.class);
String name = safeKey(key == null ? field.getName() : key.value()); // Use field name if @ConfigKey annotation is not present
}
// Check if field is table.
Table table = field.getAnnotation(Table.class);
if (table != null) {
lines.add(table.value()); // Write [name]
lines.addAll(dumpConfig(field.get(dumpable))); // Dump fields of table
continue;
}
/**
* Indicates that a field should be skipped
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Ignore {
if (field.getAnnotation(IsMap.class) != null) { // Check if field is a map
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) field.get(dumpable);
for (Entry<String, ?> entry : map.entrySet()) {
lines.add(safeKey(entry.getKey()) + " = " + serialize(entry.getValue())); // Save map data
}
lines.add(""); // Add empty line
continue;
}
}
Object value = field.get(dumpable);
/**
* Dumps this configuration to list of strings using {@link #dumpConfig(Object)}
*
* @return configuration dump
*/
public List<String> dumpConfig() {
return dumpConfig(this);
}
// Check if field is a byte[] representation of a string
if (field.getAnnotation(StringAsBytes.class) != null) {
value = new String((byte[]) value, StandardCharsets.UTF_8);
}
// Save field to config
lines.add(name + " = " + serialize(value));
lines.add(""); // Add empty line
}
} catch (IllegalAccessException | IllegalArgumentException | SecurityException e) {
throw new RuntimeException("Could not dump configuration", e);
/**
* Creates TOML configuration from supplied <pre>dumpable</pre> object.
*
* @param dumpable object which is going to be dumped
* @return string list of configuration file lines
* @throws RuntimeException if reading field value(s) fail
*/
private static List<String> dumpConfig(Object dumpable) {
List<String> lines = new ArrayList<>();
try {
for (Field field : dumpable.getClass().getDeclaredFields()) {
// Skip fields with @Ignore annotation
if (field.getAnnotation(Ignore.class) != null) {
continue;
}
return lines;
// Make field accessible
field.setAccessible(true);
// Add comments
Comment comment = field.getAnnotation(Comment.class);
if (comment != null) {
for (String line : comment.value()) {
lines.add("# " + line);
}
}
// Get a key name for config
ConfigKey key = field.getAnnotation(ConfigKey.class);
String name = safeKey(key == null ? field.getName()
: key.value()); // Use field name if @ConfigKey annotation is not present
// Check if field is table.
Table table = field.getAnnotation(Table.class);
if (table != null) {
lines.add(table.value()); // Write [name]
lines.addAll(dumpConfig(field.get(dumpable))); // Dump fields of table
continue;
}
if (field.getAnnotation(IsMap.class) != null) { // Check if field is a map
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) field.get(dumpable);
for (Entry<String, ?> entry : map.entrySet()) {
lines.add(
safeKey(entry.getKey()) + " = " + serialize(entry.getValue())); // Save map data
}
lines.add(""); // Add empty line
continue;
}
Object value = field.get(dumpable);
// Check if field is a byte[] representation of a string
if (field.getAnnotation(StringAsBytes.class) != null) {
value = new String((byte[]) value, StandardCharsets.UTF_8);
}
// Save field to config
lines.add(name + " = " + serialize(value));
lines.add(""); // Add empty line
}
} catch (IllegalAccessException | IllegalArgumentException | SecurityException e) {
throw new RuntimeException("Could not dump configuration", e);
}
/**
* Serializes <pre>value</pre> so it could be parsed by TOML specification
*
* @param value object to serialize
* @return Serialized object
*/
private static String serialize(Object value) {
if (value instanceof List) {
List<?> listValue = (List<?>) value;
if (listValue.isEmpty()) {
return "[]";
}
return lines;
}
StringBuilder m = new StringBuilder();
m.append("[");
/**
* Serializes <pre>value</pre> so it could be parsed by TOML specification
*
* @param value object to serialize
* @return Serialized object
*/
private static String serialize(Object value) {
if (value instanceof List) {
List<?> listValue = (List<?>) value;
if (listValue.isEmpty()) {
return "[]";
}
for (Object obj : listValue) {
m.append(System.lineSeparator()).append(" ").append(serialize(obj)).append(",");
}
StringBuilder m = new StringBuilder();
m.append("[");
m.deleteCharAt(m.length() - 1).append(System.lineSeparator()).append("]");
return m.toString();
}
for (Object obj : listValue) {
m.append(System.lineSeparator()).append(" ").append(serialize(obj)).append(",");
}
if (value instanceof Enum) {
value = value.toString();
}
if (value instanceof String) {
String stringValue = (String) value;
if (stringValue.isEmpty()) {
return "\"\"";
}
return "\"" + stringValue.replace("\n", "\\n") + "\"";
}
return value != null ? value.toString() : "null";
m.deleteCharAt(m.length() - 1).append(System.lineSeparator()).append("]");
return m.toString();
}
private static String safeKey(String key) {
if(key.contains(".") && !(key.indexOf('"') == 0 && key.lastIndexOf('"') == (key.length() - 1))) {
return '"' + key + '"';
}
return key;
if (value instanceof Enum) {
value = value.toString();
}
/**
* Writes list of strings to file
*
* @param lines list of strings to write
* @param to Path of file where lines should be written
* @throws IOException if error occurred during writing
* @throws IllegalArgumentException if <pre>lines</pre> is empty list
*/
public static void saveConfig(List<String> lines, Path to) throws IOException {
if (lines.isEmpty()) {
throw new IllegalArgumentException("lines cannot be empty");
}
Path temp = to.getParent().resolve(to.getFileName().toString() + "__tmp");
Files.write(temp, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
try {
Files.move(temp, to, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(temp, to, StandardCopyOption.REPLACE_EXISTING);
}
if (value instanceof String) {
String stringValue = (String) value;
if (stringValue.isEmpty()) {
return "\"\"";
}
return "\"" + stringValue.replace("\n", "\\n") + "\"";
}
return value != null ? value.toString() : "null";
}
private static String safeKey(String key) {
if (key.contains(".") && !(key.indexOf('"') == 0 && key.lastIndexOf('"') == (key.length()
- 1))) {
return '"' + key + '"';
}
return key;
}
/**
* Writes list of strings to file
*
* @param lines list of strings to write
* @param to Path of file where lines should be written
* @throws IOException if error occurred during writing
* @throws IllegalArgumentException if <pre>lines</pre> is empty list
*/
public static void saveConfig(List<String> lines, Path to) throws IOException {
if (lines.isEmpty()) {
throw new IllegalArgumentException("lines cannot be empty");
}
Path temp = to.getParent().resolve(to.getFileName().toString() + "__tmp");
Files.write(temp, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
try {
Files.move(temp, to, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(temp, to, StandardCopyOption.REPLACE_EXISTING);
}
}
}

View File

@@ -1,7 +1,7 @@
package com.velocitypowered.proxy.config;
public enum PlayerInfoForwarding {
NONE,
LEGACY,
MODERN
NONE,
LEGACY,
MODERN
}

View File

@@ -1,5 +1,14 @@
package com.velocitypowered.proxy.connection;
import static com.velocitypowered.proxy.network.Connections.CIPHER_DECODER;
import static com.velocitypowered.proxy.network.Connections.CIPHER_ENCODER;
import static com.velocitypowered.proxy.network.Connections.COMPRESSION_DECODER;
import static com.velocitypowered.proxy.network.Connections.COMPRESSION_ENCODER;
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 com.google.common.base.Preconditions;
import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.natives.encryption.VelocityCipher;
@@ -9,278 +18,288 @@ import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.*;
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftCompressDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftCompressEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.EventLoop;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.util.ReferenceCountUtil;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.GeneralSecurityException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.GeneralSecurityException;
import static com.velocitypowered.proxy.network.Connections.*;
/**
* A utility class to make working with the pipeline a little less painful and transparently handles certain Minecraft
* protocol mechanics.
* A utility class to make working with the pipeline a little less painful and transparently handles
* certain Minecraft protocol mechanics.
*/
public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
private final Channel channel;
private SocketAddress remoteAddress;
private StateRegistry state;
private @Nullable MinecraftSessionHandler sessionHandler;
private int protocolVersion;
private int nextProtocolVersion;
private @Nullable MinecraftConnectionAssociation association;
private boolean isLegacyForge;
private final VelocityServer server;
private boolean canSendLegacyFMLResetPacket = false;
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
public MinecraftConnection(Channel channel, VelocityServer server) {
this.channel = channel;
this.remoteAddress = channel.remoteAddress();
this.server = server;
this.state = StateRegistry.HANDSHAKE;
private final Channel channel;
private SocketAddress remoteAddress;
private StateRegistry state;
private @Nullable MinecraftSessionHandler sessionHandler;
private int protocolVersion;
private int nextProtocolVersion;
private @Nullable MinecraftConnectionAssociation association;
private boolean isLegacyForge;
private final VelocityServer server;
private boolean canSendLegacyFMLResetPacket = false;
public MinecraftConnection(Channel channel, VelocityServer server) {
this.channel = channel;
this.remoteAddress = channel.remoteAddress();
this.server = server;
this.state = StateRegistry.HANDSHAKE;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.connected();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.connected();
if (association != null) {
logger.info("{} has connected", association);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.disconnected();
}
if (association != null) {
logger.info("{} has disconnected", association);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (sessionHandler == null) {
// No session handler available, do nothing
ReferenceCountUtil.release(msg);
return;
}
if (msg instanceof MinecraftPacket) {
if (sessionHandler.beforeHandle()) {
return;
}
MinecraftPacket pkt = (MinecraftPacket) msg;
if (!pkt.handle(sessionHandler)) {
sessionHandler.handleGeneric((MinecraftPacket) msg);
}
} else if (msg instanceof HAProxyMessage) {
if (sessionHandler.beforeHandle()) {
return;
}
HAProxyMessage proxyMessage = (HAProxyMessage) msg;
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
proxyMessage.sourcePort());
} else if (msg instanceof ByteBuf) {
try {
if (sessionHandler.beforeHandle()) {
return;
}
sessionHandler.handleUnknown((ByteBuf) msg);
} finally {
ReferenceCountUtil.release(msg);
}
}
}
if (association != null) {
logger.info("{} has connected", association);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (ctx.channel().isActive()) {
if (sessionHandler != null) {
sessionHandler.exception(cause);
}
if (association != null) {
logger.error("{}: exception encountered", association, cause);
}
ctx.close();
}
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.writabilityChanged();
}
}
public EventLoop eventLoop() {
return channel.eventLoop();
}
public void write(Object msg) {
if (channel.isActive()) {
channel.writeAndFlush(msg, channel.voidPromise());
}
}
public void delayedWrite(Object msg) {
if (channel.isActive()) {
channel.write(msg, channel.voidPromise());
}
}
public void flush() {
if (channel.isActive()) {
channel.flush();
}
}
public void closeWith(Object msg) {
if (channel.isActive()) {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
public void close() {
if (channel.isActive()) {
channel.close();
}
}
public Channel getChannel() {
return channel;
}
public boolean isClosed() {
return !channel.isActive();
}
public SocketAddress getRemoteAddress() {
return remoteAddress;
}
public StateRegistry getState() {
return state;
}
public void setState(StateRegistry state) {
this.state = state;
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
this.channel.pipeline().get(MinecraftDecoder.class).setState(state);
}
public int getProtocolVersion() {
return protocolVersion;
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
this.nextProtocolVersion = protocolVersion;
if (protocolVersion != ProtocolConstants.LEGACY) {
this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion);
this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion);
} else {
// Legacy handshake handling
this.channel.pipeline().remove(MINECRAFT_ENCODER);
this.channel.pipeline().remove(MINECRAFT_DECODER);
}
}
@Nullable
public MinecraftSessionHandler getSessionHandler() {
return sessionHandler;
}
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
if (this.sessionHandler != null) {
this.sessionHandler.deactivated();
}
this.sessionHandler = sessionHandler;
sessionHandler.activated();
}
private void ensureOpen() {
Preconditions.checkState(!isClosed(), "Connection is closed.");
}
public void setCompressionThreshold(int threshold) {
ensureOpen();
if (threshold == -1) {
channel.pipeline().remove(COMPRESSION_DECODER);
channel.pipeline().remove(COMPRESSION_ENCODER);
return;
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.disconnected();
}
int level = server.getConfiguration().getCompressionLevel();
VelocityCompressor compressor = Natives.compressor.get().create(level);
MinecraftCompressEncoder encoder = new MinecraftCompressEncoder(threshold, compressor);
MinecraftCompressDecoder decoder = new MinecraftCompressDecoder(threshold, compressor);
if (association != null) {
logger.info("{} has disconnected", association);
}
}
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (sessionHandler == null) {
// No session handler available, do nothing
ReferenceCountUtil.release(msg);
return;
}
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
ensureOpen();
if (msg instanceof MinecraftPacket) {
if (sessionHandler.beforeHandle()) {
return;
}
SecretKey key = new SecretKeySpec(secret, "AES");
MinecraftPacket pkt = (MinecraftPacket) msg;
if (!pkt.handle(sessionHandler)) {
sessionHandler.handleGeneric((MinecraftPacket) msg);
}
} else if (msg instanceof HAProxyMessage) {
if (sessionHandler.beforeHandle()) {
return;
}
VelocityCipherFactory factory = Natives.cipher.get();
VelocityCipher decryptionCipher = factory.forDecryption(key);
VelocityCipher encryptionCipher = factory.forEncryption(key);
channel.pipeline()
.addBefore(FRAME_DECODER, CIPHER_DECODER, new MinecraftCipherDecoder(decryptionCipher));
channel.pipeline()
.addBefore(FRAME_ENCODER, CIPHER_ENCODER, new MinecraftCipherEncoder(encryptionCipher));
}
HAProxyMessage proxyMessage = (HAProxyMessage) msg;
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), proxyMessage.sourcePort());
} else if (msg instanceof ByteBuf) {
try {
if (sessionHandler.beforeHandle()) {
return;
}
sessionHandler.handleUnknown((ByteBuf) msg);
} finally {
ReferenceCountUtil.release(msg);
}
}
}
@Nullable
public MinecraftConnectionAssociation getAssociation() {
return association;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (ctx.channel().isActive()) {
if (sessionHandler != null) {
sessionHandler.exception(cause);
}
public void setAssociation(MinecraftConnectionAssociation association) {
this.association = association;
}
if (association != null) {
logger.error("{}: exception encountered", association, cause);
}
public boolean isLegacyForge() {
return isLegacyForge;
}
ctx.close();
}
}
public void setLegacyForge(boolean isForge) {
this.isLegacyForge = isForge;
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.writabilityChanged();
}
}
public boolean canSendLegacyFMLResetPacket() {
return canSendLegacyFMLResetPacket;
}
public EventLoop eventLoop() {
return channel.eventLoop();
}
public void setCanSendLegacyFMLResetPacket(boolean canSendLegacyFMLResetPacket) {
this.canSendLegacyFMLResetPacket = isLegacyForge && canSendLegacyFMLResetPacket;
}
public void write(Object msg) {
if (channel.isActive()) {
channel.writeAndFlush(msg, channel.voidPromise());
}
}
public int getNextProtocolVersion() {
return this.nextProtocolVersion;
}
public void delayedWrite(Object msg) {
if (channel.isActive()) {
channel.write(msg, channel.voidPromise());
}
}
public void flush() {
if (channel.isActive()) {
channel.flush();
}
}
public void closeWith(Object msg) {
if (channel.isActive()) {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
public void close() {
if (channel.isActive()) {
channel.close();
}
}
public Channel getChannel() {
return channel;
}
public boolean isClosed() {
return !channel.isActive();
}
public SocketAddress getRemoteAddress() {
return remoteAddress;
}
public StateRegistry getState() {
return state;
}
public void setState(StateRegistry state) {
this.state = state;
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
this.channel.pipeline().get(MinecraftDecoder.class).setState(state);
}
public int getProtocolVersion() {
return protocolVersion;
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
this.nextProtocolVersion = protocolVersion;
if (protocolVersion != ProtocolConstants.LEGACY) {
this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion);
this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion);
} else {
// Legacy handshake handling
this.channel.pipeline().remove(MINECRAFT_ENCODER);
this.channel.pipeline().remove(MINECRAFT_DECODER);
}
}
@Nullable
public MinecraftSessionHandler getSessionHandler() {
return sessionHandler;
}
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
if (this.sessionHandler != null) {
this.sessionHandler.deactivated();
}
this.sessionHandler = sessionHandler;
sessionHandler.activated();
}
private void ensureOpen() {
Preconditions.checkState(!isClosed(), "Connection is closed.");
}
public void setCompressionThreshold(int threshold) {
ensureOpen();
if (threshold == -1) {
channel.pipeline().remove(COMPRESSION_DECODER);
channel.pipeline().remove(COMPRESSION_ENCODER);
return;
}
int level = server.getConfiguration().getCompressionLevel();
VelocityCompressor compressor = Natives.compressor.get().create(level);
MinecraftCompressEncoder encoder = new MinecraftCompressEncoder(threshold, compressor);
MinecraftCompressDecoder decoder = new MinecraftCompressDecoder(threshold, compressor);
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
}
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
ensureOpen();
SecretKey key = new SecretKeySpec(secret, "AES");
VelocityCipherFactory factory = Natives.cipher.get();
VelocityCipher decryptionCipher = factory.forDecryption(key);
VelocityCipher encryptionCipher = factory.forEncryption(key);
channel.pipeline().addBefore(FRAME_DECODER, CIPHER_DECODER, new MinecraftCipherDecoder(decryptionCipher));
channel.pipeline().addBefore(FRAME_ENCODER, CIPHER_ENCODER, new MinecraftCipherEncoder(encryptionCipher));
}
@Nullable
public MinecraftConnectionAssociation getAssociation() {
return association;
}
public void setAssociation(MinecraftConnectionAssociation association) {
this.association = association;
}
public boolean isLegacyForge() {
return isLegacyForge;
}
public void setLegacyForge(boolean isForge) {
this.isLegacyForge = isForge;
}
public boolean canSendLegacyFMLResetPacket() {
return canSendLegacyFMLResetPacket;
}
public void setCanSendLegacyFMLResetPacket(boolean canSendLegacyFMLResetPacket) {
this.canSendLegacyFMLResetPacket = isLegacyForge && canSendLegacyFMLResetPacket;
}
public int getNextProtocolVersion() {
return this.nextProtocolVersion;
}
public void setNextProtocolVersion(int nextProtocolVersion) {
this.nextProtocolVersion = nextProtocolVersion;
}
public void setNextProtocolVersion(int nextProtocolVersion) {
this.nextProtocolVersion = nextProtocolVersion;
}
}

View File

@@ -1,4 +1,5 @@
package com.velocitypowered.proxy.connection;
public interface MinecraftConnectionAssociation {
}

View File

@@ -1,70 +1,173 @@
package com.velocitypowered.proxy.connection;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.protocol.packet.BossBar;
import com.velocitypowered.proxy.protocol.packet.Chat;
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequest;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponse;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter;
import com.velocitypowered.proxy.protocol.packet.JoinGame;
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
import com.velocitypowered.proxy.protocol.packet.LegacyHandshake;
import com.velocitypowered.proxy.protocol.packet.LegacyPing;
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage;
import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse;
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.Respawn;
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess;
import com.velocitypowered.proxy.protocol.packet.SetCompression;
import com.velocitypowered.proxy.protocol.packet.StatusPing;
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
import com.velocitypowered.proxy.protocol.packet.TitlePacket;
import io.netty.buffer.ByteBuf;
public interface MinecraftSessionHandler {
default boolean beforeHandle() {
return false;
}
default void handleGeneric(MinecraftPacket packet) {
default boolean beforeHandle() {
return false;
}
}
default void handleGeneric(MinecraftPacket packet) {
default void handleUnknown(ByteBuf buf) {
}
}
default void handleUnknown(ByteBuf buf) {
default void connected() {
}
}
default void connected() {
default void disconnected() {
}
}
default void disconnected() {
default void activated() {
}
}
default void activated() {
default void deactivated() {
}
}
default void deactivated() {
default void exception(Throwable throwable) {
}
}
default void exception(Throwable throwable) {
default void writabilityChanged() {
}
}
default void writabilityChanged() {
default boolean handle(BossBar packet) { return false; }
default boolean handle(Chat packet) { return false; }
default boolean handle(ClientSettings packet) { return false; }
default boolean handle(Disconnect packet) { return false; }
default boolean handle(EncryptionRequest packet) { return false; }
default boolean handle(EncryptionResponse packet) { return false; }
default boolean handle(Handshake packet) { return false; }
default boolean handle(HeaderAndFooter packet) { return false; }
default boolean handle(JoinGame packet) { return false; }
default boolean handle(KeepAlive packet) { return false; }
default boolean handle(LegacyHandshake packet) { return false; }
default boolean handle(LegacyPing packet) { return false; }
default boolean handle(LoginPluginMessage packet) { return false; }
default boolean handle(LoginPluginResponse packet) { return false; }
default boolean handle(PluginMessage packet) { return false; }
default boolean handle(Respawn packet) { return false; }
default boolean handle(ServerLogin packet) { return false; }
default boolean handle(ServerLoginSuccess packet) { return false; }
default boolean handle(SetCompression packet) { return false; }
default boolean handle(StatusPing packet) { return false; }
default boolean handle(StatusRequest packet) { return false; }
default boolean handle(StatusResponse packet) { return false; }
default boolean handle(TabCompleteRequest packet) { return false; }
default boolean handle(TabCompleteResponse packet) { return false; }
default boolean handle(TitlePacket packet) { return false; }
default boolean handle(PlayerListItem packet) { return false; }
}
default boolean handle(BossBar packet) {
return false;
}
default boolean handle(Chat packet) {
return false;
}
default boolean handle(ClientSettings packet) {
return false;
}
default boolean handle(Disconnect packet) {
return false;
}
default boolean handle(EncryptionRequest packet) {
return false;
}
default boolean handle(EncryptionResponse packet) {
return false;
}
default boolean handle(Handshake packet) {
return false;
}
default boolean handle(HeaderAndFooter packet) {
return false;
}
default boolean handle(JoinGame packet) {
return false;
}
default boolean handle(KeepAlive packet) {
return false;
}
default boolean handle(LegacyHandshake packet) {
return false;
}
default boolean handle(LegacyPing packet) {
return false;
}
default boolean handle(LoginPluginMessage packet) {
return false;
}
default boolean handle(LoginPluginResponse packet) {
return false;
}
default boolean handle(PluginMessage packet) {
return false;
}
default boolean handle(Respawn packet) {
return false;
}
default boolean handle(ServerLogin packet) {
return false;
}
default boolean handle(ServerLoginSuccess packet) {
return false;
}
default boolean handle(SetCompression packet) {
return false;
}
default boolean handle(StatusPing packet) {
return false;
}
default boolean handle(StatusRequest packet) {
return false;
}
default boolean handle(StatusResponse packet) {
return false;
}
default boolean handle(TabCompleteRequest packet) {
return false;
}
default boolean handle(TabCompleteResponse packet) {
return false;
}
default boolean handle(TitlePacket packet) {
return false;
}
default boolean handle(PlayerListItem packet) {
return false;
}
}

View File

@@ -1,12 +1,13 @@
package com.velocitypowered.proxy.connection;
public class VelocityConstants {
private VelocityConstants() {
throw new AssertionError();
}
public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info";
public static final int FORWARDING_VERSION = 1;
private VelocityConstants() {
throw new AssertionError();
}
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info";
public static final int FORWARDING_VERSION = 1;
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
}

View File

@@ -10,167 +10,179 @@ import com.velocitypowered.proxy.connection.forge.ForgeConstants;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.protocol.packet.BossBar;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.JoinGame;
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf;
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
private final VelocityServer server;
private final VelocityServerConnection serverConn;
private final ClientPlaySessionHandler playerSessionHandler;
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
this.server = server;
this.serverConn = serverConn;
private final VelocityServer server;
private final VelocityServerConnection serverConn;
private final ClientPlaySessionHandler playerSessionHandler;
MinecraftSessionHandler psh = serverConn.getPlayer().getConnection().getSessionHandler();
if (!(psh instanceof ClientPlaySessionHandler)) {
throw new IllegalStateException("Initializing BackendPlaySessionHandler with no backing client play session handler!");
}
this.playerSessionHandler = (ClientPlaySessionHandler) psh;
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
this.server = server;
this.serverConn = serverConn;
MinecraftSessionHandler psh = serverConn.getPlayer().getConnection().getSessionHandler();
if (!(psh instanceof ClientPlaySessionHandler)) {
throw new IllegalStateException(
"Initializing BackendPlaySessionHandler with no backing client play session handler!");
}
this.playerSessionHandler = (ClientPlaySessionHandler) psh;
}
@Override
public void activated() {
serverConn.getServer().addPlayer(serverConn.getPlayer());
}
@Override
public boolean beforeHandle() {
if (!serverConn.getPlayer().isActive()) {
// Obsolete connection
serverConn.disconnect();
return true;
}
return false;
}
@Override
public boolean handle(KeepAlive packet) {
serverConn.setLastPingId(packet.getRandomId());
return false; // forwards on
}
@Override
public boolean handle(Disconnect packet) {
serverConn.disconnect();
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), packet);
return true;
}
@Override
public boolean handle(JoinGame packet) {
playerSessionHandler.handleBackendJoinGame(packet);
return true;
}
@Override
public boolean handle(BossBar packet) {
if (packet.getAction() == BossBar.ADD) {
playerSessionHandler.getServerBossBars().add(packet.getUuid());
} else if (packet.getAction() == BossBar.REMOVE) {
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
}
return false; // forward
}
@Override
public boolean handle(PluginMessage packet) {
MinecraftConnection smc = serverConn.getConnection();
if (smc == null) {
return true;
}
@Override
public void activated() {
serverConn.getServer().addPlayer(serverConn.getPlayer());
if (!canForwardPluginMessage(packet)) {
return true;
}
@Override
public boolean beforeHandle() {
if (!serverConn.getPlayer().isActive()) {
// Obsolete connection
serverConn.disconnect();
return true;
}
return false;
if (PluginMessageUtil.isMCBrand(packet)) {
serverConn.getPlayer().getConnection().write(PluginMessageUtil.rewriteMCBrand(packet));
return true;
}
@Override
public boolean handle(KeepAlive packet) {
serverConn.setLastPingId(packet.getRandomId());
return false; // forwards on
if (!serverConn.hasCompletedJoin() && packet.getChannel()
.equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
if (!serverConn.isLegacyForge()) {
serverConn.setLegacyForge(true);
// We must always reset the handshake before a modded connection is established if
// we haven't done so already.
serverConn.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
// Always forward these messages during login.
return false;
}
@Override
public boolean handle(Disconnect packet) {
serverConn.disconnect();
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), packet);
return true;
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
return false;
}
@Override
public boolean handle(JoinGame packet) {
playerSessionHandler.handleBackendJoinGame(packet);
return true;
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id,
packet.getData());
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
smc.write(packet);
}
}, smc.eventLoop());
return true;
}
@Override
public boolean handle(TabCompleteResponse packet) {
playerSessionHandler.handleTabCompleteResponse(packet);
return true;
}
@Override
public boolean handle(PlayerListItem packet) {
serverConn.getPlayer().getTabList().processBackendPacket(packet);
return false; //Forward packet to player
}
@Override
public void handleGeneric(MinecraftPacket packet) {
serverConn.getPlayer().getConnection().write(packet);
}
@Override
public void handleUnknown(ByteBuf buf) {
serverConn.getPlayer().getConnection().write(buf.retain());
}
@Override
public void exception(Throwable throwable) {
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), throwable);
}
public VelocityServer getServer() {
return server;
}
@Override
public void disconnected() {
serverConn.getServer().removePlayer(serverConn.getPlayer());
if (!serverConn.isGracefulDisconnect()) {
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), Disconnect.create(
ConnectionMessages.UNEXPECTED_DISCONNECT));
}
}
@Override
public boolean handle(BossBar packet) {
if (packet.getAction() == BossBar.ADD) {
playerSessionHandler.getServerBossBars().add(packet.getUuid());
} else if (packet.getAction() == BossBar.REMOVE) {
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
}
return false; // forward
private boolean canForwardPluginMessage(PluginMessage message) {
MinecraftConnection mc = serverConn.getConnection();
if (mc == null) {
return false;
}
@Override
public boolean handle(PluginMessage packet) {
MinecraftConnection smc = serverConn.getConnection();
if (smc == null) {
return true;
}
if (!canForwardPluginMessage(packet)) {
return true;
}
if (PluginMessageUtil.isMCBrand(packet)) {
serverConn.getPlayer().getConnection().write(PluginMessageUtil.rewriteMCBrand(packet));
return true;
}
if (!serverConn.hasCompletedJoin() && packet.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
if (!serverConn.isLegacyForge()) {
serverConn.setLegacyForge(true);
// We must always reset the handshake before a modded connection is established if
// we haven't done so already.
serverConn.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
// Always forward these messages during login.
return false;
}
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
return false;
}
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, packet.getData());
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
smc.write(packet);
}
}, smc.eventLoop());
return true;
}
@Override
public boolean handle(TabCompleteResponse packet) {
playerSessionHandler.handleTabCompleteResponse(packet);
return true;
}
@Override
public boolean handle(PlayerListItem packet) {
serverConn.getPlayer().getTabList().processBackendPacket(packet);
return false; //Forward packet to player
}
@Override
public void handleGeneric(MinecraftPacket packet) {
serverConn.getPlayer().getConnection().write(packet);
}
@Override
public void handleUnknown(ByteBuf buf) {
serverConn.getPlayer().getConnection().write(buf.retain());
}
@Override
public void exception(Throwable throwable) {
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), throwable);
}
public VelocityServer getServer() {
return server;
}
@Override
public void disconnected() {
serverConn.getServer().removePlayer(serverConn.getPlayer());
if (!serverConn.isGracefulDisconnect()) {
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), Disconnect.create(
ConnectionMessages.UNEXPECTED_DISCONNECT));
}
}
private boolean canForwardPluginMessage(PluginMessage message) {
MinecraftConnection mc = serverConn.getConnection();
if (mc == null) {
return false;
}
boolean isMCOrFMLMessage;
if (mc.getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) {
String channel = message.getChannel();
isMCOrFMLMessage = channel.startsWith("MC|") || channel.startsWith(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL);
} else {
isMCOrFMLMessage = message.getChannel().startsWith("minecraft:");
}
return isMCOrFMLMessage || playerSessionHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
server.getChannelRegistrar().registered(message.getChannel());
boolean isMCOrFMLMessage;
if (mc.getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) {
String channel = message.getChannel();
isMCOrFMLMessage = channel.startsWith("MC|") || channel
.startsWith(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL);
} else {
isMCOrFMLMessage = message.getChannel().startsWith("minecraft:");
}
return isMCOrFMLMessage || playerSessionHandler.getClientPluginMsgChannels()
.contains(message.getChannel()) ||
server.getChannelRegistrar().registered(message.getChannel());
}
}

View File

@@ -13,156 +13,168 @@ import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequest;
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage;
import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess;
import com.velocitypowered.proxy.protocol.packet.SetCompression;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.kyori.text.TextComponent;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CompletableFuture;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import net.kyori.text.TextComponent;
public class LoginSessionHandler implements MinecraftSessionHandler {
private final VelocityServer server;
private final VelocityServerConnection serverConn;
private final CompletableFuture<ConnectionRequestBuilder.Result> resultFuture;
private boolean informationForwarded;
LoginSessionHandler(VelocityServer server, VelocityServerConnection serverConn,
CompletableFuture<ConnectionRequestBuilder.Result> resultFuture) {
this.server = server;
this.serverConn = serverConn;
this.resultFuture = resultFuture;
private final VelocityServer server;
private final VelocityServerConnection serverConn;
private final CompletableFuture<ConnectionRequestBuilder.Result> resultFuture;
private boolean informationForwarded;
LoginSessionHandler(VelocityServer server, VelocityServerConnection serverConn,
CompletableFuture<ConnectionRequestBuilder.Result> resultFuture) {
this.server = server;
this.serverConn = serverConn;
this.resultFuture = resultFuture;
}
private MinecraftConnection ensureMinecraftConnection() {
MinecraftConnection mc = serverConn.getConnection();
if (mc == null) {
throw new IllegalStateException("Not connected to backend server!");
}
return mc;
}
@Override
public boolean handle(EncryptionRequest packet) {
throw new IllegalStateException("Backend server is online-mode!");
}
@Override
public boolean handle(LoginPluginMessage packet) {
MinecraftConnection mc = ensureMinecraftConnection();
VelocityConfiguration configuration = server.getConfiguration();
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && packet
.getChannel()
.equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(true);
response.setId(packet.getId());
response.setData(createForwardingData(configuration.getForwardingSecret(),
serverConn.getPlayer().getRemoteAddress().getHostString(),
serverConn.getPlayer().getProfile()));
mc.write(response);
informationForwarded = true;
} else {
// Don't understand
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(false);
response.setId(packet.getId());
response.setData(Unpooled.EMPTY_BUFFER);
mc.write(response);
}
return true;
}
@Override
public boolean handle(Disconnect packet) {
resultFuture.complete(ConnectionRequestResults.forDisconnect(packet));
serverConn.disconnect();
return true;
}
@Override
public boolean handle(SetCompression packet) {
ensureMinecraftConnection().setCompressionThreshold(packet.getThreshold());
return true;
}
@Override
public boolean handle(ServerLoginSuccess packet) {
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN
&& !informationForwarded) {
resultFuture.complete(ConnectionRequestResults.forDisconnect(
TextComponent
.of("Your server did not send a forwarding request to the proxy. Is it set up correctly?")));
serverConn.disconnect();
return true;
}
private MinecraftConnection ensureMinecraftConnection() {
MinecraftConnection mc = serverConn.getConnection();
if (mc == null) {
throw new IllegalStateException("Not connected to backend server!");
}
return mc;
// The player has been logged on to the backend server.
MinecraftConnection smc = ensureMinecraftConnection();
smc.setState(StateRegistry.PLAY);
VelocityServerConnection existingConnection = serverConn.getPlayer().getConnectedServer();
if (existingConnection == null) {
// Strap on the play session handler
serverConn.getPlayer().getConnection()
.setSessionHandler(new ClientPlaySessionHandler(server, serverConn.getPlayer()));
} else {
// The previous server connection should become obsolete.
// Before we remove it, if the server we are departing is modded, we must always reset the client state.
if (existingConnection.isLegacyForge()) {
serverConn.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
existingConnection.disconnect();
}
@Override
public boolean handle(EncryptionRequest packet) {
throw new IllegalStateException("Backend server is online-mode!");
}
@Override
public boolean handle(LoginPluginMessage packet) {
MinecraftConnection mc = ensureMinecraftConnection();
VelocityConfiguration configuration = server.getConfiguration();
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && packet.getChannel()
.equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(true);
response.setId(packet.getId());
response.setData(createForwardingData(configuration.getForwardingSecret(),
serverConn.getPlayer().getRemoteAddress().getHostString(),
serverConn.getPlayer().getProfile()));
mc.write(response);
informationForwarded = true;
} else {
// Don't understand
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(false);
response.setId(packet.getId());
response.setData(Unpooled.EMPTY_BUFFER);
mc.write(response);
}
return true;
}
@Override
public boolean handle(Disconnect packet) {
resultFuture.complete(ConnectionRequestResults.forDisconnect(packet));
serverConn.disconnect();
return true;
}
@Override
public boolean handle(SetCompression packet) {
ensureMinecraftConnection().setCompressionThreshold(packet.getThreshold());
return true;
}
@Override
public boolean handle(ServerLoginSuccess packet) {
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && !informationForwarded) {
resultFuture.complete(ConnectionRequestResults.forDisconnect(
TextComponent.of("Your server did not send a forwarding request to the proxy. Is it set up correctly?")));
serverConn.disconnect();
return true;
}
// The player has been logged on to the backend server.
MinecraftConnection smc = ensureMinecraftConnection();
smc.setState(StateRegistry.PLAY);
VelocityServerConnection existingConnection = serverConn.getPlayer().getConnectedServer();
if (existingConnection == null) {
// Strap on the play session handler
serverConn.getPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(server, serverConn.getPlayer()));
} else {
// The previous server connection should become obsolete.
// Before we remove it, if the server we are departing is modded, we must always reset the client state.
if (existingConnection.isLegacyForge()) {
serverConn.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
existingConnection.disconnect();
}
smc.getChannel().config().setAutoRead(false);
server.getEventManager().fire(new ServerConnectedEvent(serverConn.getPlayer(), serverConn.getServer()))
.whenCompleteAsync((x, error) -> {
resultFuture.complete(ConnectionRequestResults.SUCCESSFUL);
smc.setSessionHandler(new BackendPlaySessionHandler(server, serverConn));
serverConn.getPlayer().setConnectedServer(serverConn);
smc.getChannel().config().setAutoRead(true);
}, smc.eventLoop());
return true;
}
@Override
public void exception(Throwable throwable) {
resultFuture.completeExceptionally(throwable);
}
@Override
public void disconnected() {
resultFuture.completeExceptionally(new IOException("Unexpectedly disconnected from remote server"));
}
private static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
ByteBuf dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer();
try {
ProtocolUtils.writeVarInt(dataToForward, VelocityConstants.FORWARDING_VERSION);
ProtocolUtils.writeString(dataToForward, address);
ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid());
ProtocolUtils.writeString(dataToForward, profile.getName());
ProtocolUtils.writeProperties(dataToForward, profile.getProperties());
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
byte[] sig = mac.doFinal();
finalData.writeBytes(sig);
finalData.writeBytes(dataToForward);
return finalData;
} catch (InvalidKeyException e) {
finalData.release();
throw new RuntimeException("Unable to authenticate data", e);
} catch (NoSuchAlgorithmException e) {
// Should never happen
finalData.release();
throw new AssertionError(e);
} finally {
dataToForward.release();
}
smc.getChannel().config().setAutoRead(false);
server.getEventManager()
.fire(new ServerConnectedEvent(serverConn.getPlayer(), serverConn.getServer()))
.whenCompleteAsync((x, error) -> {
resultFuture.complete(ConnectionRequestResults.SUCCESSFUL);
smc.setSessionHandler(new BackendPlaySessionHandler(server, serverConn));
serverConn.getPlayer().setConnectedServer(serverConn);
smc.getChannel().config().setAutoRead(true);
}, smc.eventLoop());
return true;
}
@Override
public void exception(Throwable throwable) {
resultFuture.completeExceptionally(throwable);
}
@Override
public void disconnected() {
resultFuture
.completeExceptionally(new IOException("Unexpectedly disconnected from remote server"));
}
private static ByteBuf createForwardingData(byte[] hmacSecret, String address,
GameProfile profile) {
ByteBuf dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer();
try {
ProtocolUtils.writeVarInt(dataToForward, VelocityConstants.FORWARDING_VERSION);
ProtocolUtils.writeString(dataToForward, address);
ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid());
ProtocolUtils.writeString(dataToForward, profile.getName());
ProtocolUtils.writeProperties(dataToForward, profile.getProperties());
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
byte[] sig = mac.doFinal();
finalData.writeBytes(sig);
finalData.writeBytes(dataToForward);
return finalData;
} catch (InvalidKeyException e) {
finalData.release();
throw new RuntimeException("Unable to authenticate data", e);
} catch (NoSuchAlgorithmException e) {
// Should never happen
finalData.release();
throw new AssertionError(e);
} finally {
dataToForward.release();
}
}
}

View File

@@ -1,5 +1,13 @@
package com.velocitypowered.proxy.connection.backend;
import static com.velocitypowered.proxy.VelocityServer.GSON;
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.HANDLER;
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.google.common.base.Preconditions;
import com.google.common.base.VerifyException;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
@@ -25,193 +33,196 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.timeout.ReadTimeoutHandler;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Verify.verify;
import static com.velocitypowered.proxy.VelocityServer.GSON;
import static com.velocitypowered.proxy.network.Connections.*;
import org.checkerframework.checker.nullness.qual.Nullable;
public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection {
private final VelocityRegisteredServer registeredServer;
private final ConnectedPlayer proxyPlayer;
private final VelocityServer server;
private @Nullable MinecraftConnection connection;
private boolean legacyForge = false;
private boolean hasCompletedJoin = false;
private boolean gracefulDisconnect = false;
private long lastPingId;
private long lastPingSent;
public VelocityServerConnection(VelocityRegisteredServer registeredServer, ConnectedPlayer proxyPlayer, VelocityServer server) {
this.registeredServer = registeredServer;
this.proxyPlayer = proxyPlayer;
this.server = server;
private final VelocityRegisteredServer registeredServer;
private final ConnectedPlayer proxyPlayer;
private final VelocityServer server;
private @Nullable MinecraftConnection connection;
private boolean legacyForge = false;
private boolean hasCompletedJoin = false;
private boolean gracefulDisconnect = false;
private long lastPingId;
private long lastPingSent;
public VelocityServerConnection(VelocityRegisteredServer registeredServer,
ConnectedPlayer proxyPlayer, VelocityServer server) {
this.registeredServer = registeredServer;
this.proxyPlayer = proxyPlayer;
this.server = server;
}
public CompletableFuture<ConnectionRequestBuilder.Result> connect() {
CompletableFuture<ConnectionRequestBuilder.Result> result = new CompletableFuture<>();
server.initializeGenericBootstrap()
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(READ_TIMEOUT,
new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(),
TimeUnit.SECONDS))
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER,
new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
.addLast(MINECRAFT_ENCODER,
new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
MinecraftConnection mc = new MinecraftConnection(ch, server);
mc.setState(StateRegistry.HANDSHAKE);
mc.setAssociation(VelocityServerConnection.this);
ch.pipeline().addLast(HANDLER, mc);
}
})
.connect(registeredServer.getServerInfo().getAddress())
.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
connection = future.channel().pipeline().get(MinecraftConnection.class);
// This is guaranteed not to be null, but Checker Framework is whining about it anyway
if (connection == null) {
throw new VerifyException("MinecraftConnection not injected into pipeline");
}
// Kick off the connection process
connection.setSessionHandler(
new LoginSessionHandler(server, VelocityServerConnection.this, result));
startHandshake();
} else {
result.completeExceptionally(future.cause());
}
});
return result;
}
private String createBungeeForwardingAddress() {
// BungeeCord IP forwarding is simply a special injection after the "address" in the handshake,
// separated by \0 (the null byte). In order, you send the original host, the player's IP, their
// UUID (undashed), and if you are in online-mode, their login properties (retrieved from Mojang).
return registeredServer.getServerInfo().getAddress().getHostString() + "\0" +
proxyPlayer.getRemoteAddress().getHostString() + "\0" +
proxyPlayer.getProfile().getId() + "\0" +
GSON.toJson(proxyPlayer.getProfile().getProperties());
}
private void startHandshake() {
MinecraftConnection mc = connection;
if (mc == null) {
throw new IllegalStateException("No connection established!");
}
public CompletableFuture<ConnectionRequestBuilder.Result> connect() {
CompletableFuture<ConnectionRequestBuilder.Result> result = new CompletableFuture<>();
server.initializeGenericBootstrap()
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS))
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
MinecraftConnection mc = new MinecraftConnection(ch, server);
mc.setState(StateRegistry.HANDSHAKE);
mc.setAssociation(VelocityServerConnection.this);
ch.pipeline().addLast(HANDLER, mc);
}
})
.connect(registeredServer.getServerInfo().getAddress())
.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
connection = future.channel().pipeline().get(MinecraftConnection.class);
// Initiate a handshake.
Handshake handshake = new Handshake();
handshake.setNextStatus(StateRegistry.LOGIN_ID);
handshake.setProtocolVersion(proxyPlayer.getConnection().getNextProtocolVersion());
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createBungeeForwardingAddress());
} else if (proxyPlayer.getConnection().isLegacyForge()) {
handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0");
} else {
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
}
handshake.setPort(registeredServer.getServerInfo().getAddress().getPort());
mc.write(handshake);
// This is guaranteed not to be null, but Checker Framework is whining about it anyway
if (connection == null) {
throw new VerifyException("MinecraftConnection not injected into pipeline");
}
int protocolVersion = proxyPlayer.getConnection().getNextProtocolVersion();
mc.setProtocolVersion(protocolVersion);
mc.setState(StateRegistry.LOGIN);
mc.write(new ServerLogin(proxyPlayer.getUsername()));
}
// Kick off the connection process
connection.setSessionHandler(new LoginSessionHandler(server, VelocityServerConnection.this, result));
startHandshake();
} else {
result.completeExceptionally(future.cause());
}
});
return result;
@Nullable
public MinecraftConnection getConnection() {
return connection;
}
@Override
public VelocityRegisteredServer getServer() {
return registeredServer;
}
@Override
public ServerInfo getServerInfo() {
return registeredServer.getServerInfo();
}
@Override
public ConnectedPlayer getPlayer() {
return proxyPlayer;
}
public void disconnect() {
if (connection != null) {
connection.close();
connection = null;
gracefulDisconnect = true;
}
}
@Override
public String toString() {
return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + registeredServer
.getServerInfo().getName();
}
@Override
public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
Preconditions.checkNotNull(identifier, "identifier");
Preconditions.checkNotNull(data, "data");
MinecraftConnection mc = connection;
if (mc == null) {
throw new IllegalStateException("Not connected to a server!");
}
private String createBungeeForwardingAddress() {
// BungeeCord IP forwarding is simply a special injection after the "address" in the handshake,
// separated by \0 (the null byte). In order, you send the original host, the player's IP, their
// UUID (undashed), and if you are in online-mode, their login properties (retrieved from Mojang).
return registeredServer.getServerInfo().getAddress().getHostString() + "\0" +
proxyPlayer.getRemoteAddress().getHostString() + "\0" +
proxyPlayer.getProfile().getId() + "\0" +
GSON.toJson(proxyPlayer.getProfile().getProperties());
}
PluginMessage message = new PluginMessage();
message.setChannel(identifier.getId());
message.setData(data);
mc.write(message);
return true;
}
private void startHandshake() {
MinecraftConnection mc = connection;
if (mc == null) {
throw new IllegalStateException("No connection established!");
}
public boolean isLegacyForge() {
return legacyForge;
}
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
public void setLegacyForge(boolean modded) {
legacyForge = modded;
}
// Initiate a handshake.
Handshake handshake = new Handshake();
handshake.setNextStatus(StateRegistry.LOGIN_ID);
handshake.setProtocolVersion(proxyPlayer.getConnection().getNextProtocolVersion());
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createBungeeForwardingAddress());
} else if (proxyPlayer.getConnection().isLegacyForge()) {
handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0");
} else {
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
}
handshake.setPort(registeredServer.getServerInfo().getAddress().getPort());
mc.write(handshake);
public boolean hasCompletedJoin() {
return hasCompletedJoin;
}
int protocolVersion = proxyPlayer.getConnection().getNextProtocolVersion();
mc.setProtocolVersion(protocolVersion);
mc.setState(StateRegistry.LOGIN);
mc.write(new ServerLogin(proxyPlayer.getUsername()));
}
public void setHasCompletedJoin(boolean hasCompletedJoin) {
this.hasCompletedJoin = hasCompletedJoin;
}
@Nullable
public MinecraftConnection getConnection() {
return connection;
}
public boolean isGracefulDisconnect() {
return gracefulDisconnect;
}
@Override
public VelocityRegisteredServer getServer() {
return registeredServer;
}
public long getLastPingId() {
return lastPingId;
}
@Override
public ServerInfo getServerInfo() {
return registeredServer.getServerInfo();
}
public long getLastPingSent() {
return lastPingSent;
}
@Override
public ConnectedPlayer getPlayer() {
return proxyPlayer;
}
public void setLastPingId(long lastPingId) {
this.lastPingId = lastPingId;
this.lastPingSent = System.currentTimeMillis();
}
public void disconnect() {
if (connection != null) {
connection.close();
connection = null;
gracefulDisconnect = true;
}
}
@Override
public String toString() {
return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + registeredServer.getServerInfo().getName();
}
@Override
public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
Preconditions.checkNotNull(identifier, "identifier");
Preconditions.checkNotNull(data, "data");
MinecraftConnection mc = connection;
if (mc == null) {
throw new IllegalStateException("Not connected to a server!");
}
PluginMessage message = new PluginMessage();
message.setChannel(identifier.getId());
message.setData(data);
mc.write(message);
return true;
}
public boolean isLegacyForge() {
return legacyForge;
}
public void setLegacyForge(boolean modded) {
legacyForge = modded;
}
public boolean hasCompletedJoin() {
return hasCompletedJoin;
}
public void setHasCompletedJoin(boolean hasCompletedJoin) {
this.hasCompletedJoin = hasCompletedJoin;
}
public boolean isGracefulDisconnect() {
return gracefulDisconnect;
}
public long getLastPingId() {
return lastPingId;
}
public long getLastPingSent() {
return lastPingSent;
}
public void setLastPingId(long lastPingId) {
this.lastPingId = lastPingId;
this.lastPingSent = System.currentTimeMillis();
}
public void resetLastPingId() {
this.lastPingId = -1;
}
public void resetLastPingId() {
this.lastPingId = -1;
}
}

View File

@@ -12,357 +12,388 @@ import com.velocitypowered.proxy.connection.forge.ForgeConstants;
import com.velocitypowered.proxy.connection.forge.ForgeUtil;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.protocol.packet.BossBar;
import com.velocitypowered.proxy.protocol.packet.Chat;
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
import com.velocitypowered.proxy.protocol.packet.JoinGame;
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.Respawn;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
import com.velocitypowered.proxy.protocol.packet.TitlePacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import com.velocitypowered.proxy.util.ThrowableUtils;
import io.netty.buffer.ByteBuf;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.*;
/**
* Handles communication with the connected Minecraft client. This is effectively the primary nerve center that
* joins backend servers with players.
* Handles communication with the connected Minecraft client. This is effectively the primary nerve
* center that joins backend servers with players.
*/
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
private static final int MAX_PLUGIN_CHANNELS = 1024;
private final ConnectedPlayer player;
private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>();
private final Set<String> clientPluginMsgChannels = new HashSet<>();
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
private final VelocityServer server;
private @Nullable TabCompleteRequest outstandingTabComplete;
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
private static final int MAX_PLUGIN_CHANNELS = 1024;
public ClientPlaySessionHandler(VelocityServer server, ConnectedPlayer player) {
this.player = player;
this.server = server;
private final ConnectedPlayer player;
private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>();
private final Set<String> clientPluginMsgChannels = new HashSet<>();
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
private final VelocityServer server;
private @Nullable TabCompleteRequest outstandingTabComplete;
public ClientPlaySessionHandler(VelocityServer server, ConnectedPlayer player) {
this.player = player;
this.server = server;
}
@Override
public void activated() {
PluginMessage register = PluginMessageUtil.constructChannelsPacket(player.getProtocolVersion(),
server.getChannelRegistrar().getModernChannelIds());
player.getConnection().write(register);
}
@Override
public boolean handle(KeepAlive packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection != null && packet.getRandomId() == serverConnection.getLastPingId()) {
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null) {
player.setPing(System.currentTimeMillis() - serverConnection.getLastPingSent());
smc.write(packet);
serverConnection.resetLastPingId();
}
}
return true;
}
@Override
public void activated() {
PluginMessage register = PluginMessageUtil.constructChannelsPacket(player.getProtocolVersion(), server.getChannelRegistrar().getModernChannelIds());
player.getConnection().write(register);
}
@Override
public boolean handle(ClientSettings packet) {
player.setPlayerSettings(packet);
return false; // will forward onto the handleGeneric below, which will write the packet to the remote server
}
@Override
public boolean handle(KeepAlive packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection != null && packet.getRandomId() == serverConnection.getLastPingId()) {
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null) {
player.setPing(System.currentTimeMillis() - serverConnection.getLastPingSent());
@Override
public boolean handle(Chat packet) {
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.getProfile().getName(),
e);
player.sendMessage(
TextComponent.of("An error occurred while running this command.", TextColor.RED));
return true;
}
} else {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
return true;
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc == null) {
return true;
}
PlayerChatEvent event = new PlayerChatEvent(player, msg);
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
PlayerChatEvent.ChatResult chatResult = pme.getResult();
if (chatResult.isAllowed()) {
Optional<String> eventMsg = pme.getResult().getMessage();
if (eventMsg.isPresent()) {
smc.write(Chat.createServerbound(eventMsg.get()));
} else {
smc.write(packet);
serverConnection.resetLastPingId();
}
}
}, smc.eventLoop());
}
return true;
}
@Override
public boolean handle(TabCompleteRequest packet) {
// Record the request so that the outstanding request can be augmented later.
if (!packet.isAssumeCommand() && packet.getCommand().startsWith("/")) {
int spacePos = packet.getCommand().indexOf(' ');
if (spacePos > 0) {
String cmd = packet.getCommand().substring(1, spacePos);
if (server.getCommandManager().hasCommand(cmd)) {
List<String> suggestions = server.getCommandManager()
.offerSuggestions(player, packet.getCommand().substring(1));
if (!suggestions.isEmpty()) {
TabCompleteResponse resp = new TabCompleteResponse();
resp.getOffers().addAll(suggestions);
player.getConnection().write(resp);
return true;
}
}
return true;
}
}
outstandingTabComplete = packet;
return false;
}
@Override
public boolean handle(ClientSettings packet) {
player.setPlayerSettings(packet);
return false; // will forward onto the handleGeneric below, which will write the packet to the remote server
}
@Override
public boolean handle(PluginMessage packet) {
VelocityServerConnection serverConn = player.getConnectedServer();
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
if (serverConn != null && backendConn != null) {
if (PluginMessageUtil.isMCRegister(packet)) {
List<String> actuallyRegistered = new ArrayList<>();
List<String> channels = PluginMessageUtil.getChannels(packet);
for (String channel : channels) {
if (clientPluginMsgChannels.size() >= MAX_PLUGIN_CHANNELS &&
!clientPluginMsgChannels.contains(channel)) {
throw new IllegalStateException("Too many plugin message channels registered");
}
if (clientPluginMsgChannels.add(channel)) {
actuallyRegistered.add(channel);
}
}
@Override
public boolean handle(Chat packet) {
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.getProfile().getName(), e);
player.sendMessage(TextComponent.of("An error occurred while running this command.", TextColor.RED));
return true;
if (!actuallyRegistered.isEmpty()) {
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(backendConn
.getProtocolVersion(), actuallyRegistered);
backendConn.write(newRegisterPacket);
}
} else if (PluginMessageUtil.isMCUnregister(packet)) {
List<String> channels = PluginMessageUtil.getChannels(packet);
clientPluginMsgChannels.removeAll(channels);
backendConn.write(packet);
} else if (PluginMessageUtil.isMCBrand(packet)) {
backendConn.write(PluginMessageUtil.rewriteMCBrand(packet));
} else if (backendConn.isLegacyForge() && !serverConn.hasCompletedJoin()) {
if (packet.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
if (!player.getModInfo().isPresent()) {
List<ModInfo.Mod> mods = ForgeUtil.readModList(packet);
if (!mods.isEmpty()) {
player.setModInfo(new ModInfo("FML", mods));
}
}
// Always forward the FML handshake to the remote server.
backendConn.write(packet);
} else {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
return true;
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc == null) {
return true;
}
PlayerChatEvent event = new PlayerChatEvent(player, msg);
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
PlayerChatEvent.ChatResult chatResult = pme.getResult();
if (chatResult.isAllowed()) {
Optional<String> eventMsg = pme.getResult().getMessage();
if (eventMsg.isPresent()) {
smc.write(Chat.createServerbound(eventMsg.get()));
} else {
smc.write(packet);
}
}
}, smc.eventLoop());
// The client is trying to send messages too early. This is primarily caused by mods, but it's further
// aggravated by Velocity. To work around these issues, we will queue any non-FML handshake messages to
// be sent once the JoinGame packet has been received by the proxy.
loginPluginMessages.add(packet);
}
return true;
}
@Override
public boolean handle(TabCompleteRequest packet) {
// Record the request so that the outstanding request can be augmented later.
if (!packet.isAssumeCommand() && packet.getCommand().startsWith("/")) {
int spacePos = packet.getCommand().indexOf(' ');
if (spacePos > 0) {
String cmd = packet.getCommand().substring(1, spacePos);
if (server.getCommandManager().hasCommand(cmd)) {
List<String> suggestions = server.getCommandManager().offerSuggestions(player, packet.getCommand().substring(1));
if (!suggestions.isEmpty()) {
TabCompleteResponse resp = new TabCompleteResponse();
resp.getOffers().addAll(suggestions);
player.getConnection().write(resp);
return true;
}
}
}
}
outstandingTabComplete = packet;
return false;
}
@Override
public boolean handle(PluginMessage packet) {
VelocityServerConnection serverConn = player.getConnectedServer();
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
if (serverConn != null && backendConn != null) {
if (PluginMessageUtil.isMCRegister(packet)) {
List<String> actuallyRegistered = new ArrayList<>();
List<String> channels = PluginMessageUtil.getChannels(packet);
for (String channel : channels) {
if (clientPluginMsgChannels.size() >= MAX_PLUGIN_CHANNELS &&
!clientPluginMsgChannels.contains(channel)) {
throw new IllegalStateException("Too many plugin message channels registered");
}
if (clientPluginMsgChannels.add(channel)) {
actuallyRegistered.add(channel);
}
}
if (!actuallyRegistered.isEmpty()) {
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(backendConn
.getProtocolVersion(), actuallyRegistered);
backendConn.write(newRegisterPacket);
}
} else if (PluginMessageUtil.isMCUnregister(packet)) {
List<String> channels = PluginMessageUtil.getChannels(packet);
clientPluginMsgChannels.removeAll(channels);
backendConn.write(packet);
} else if (PluginMessageUtil.isMCBrand(packet)) {
backendConn.write(PluginMessageUtil.rewriteMCBrand(packet));
} else if (backendConn.isLegacyForge() && !serverConn.hasCompletedJoin()) {
if (packet.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
if (!player.getModInfo().isPresent()) {
List<ModInfo.Mod> mods = ForgeUtil.readModList(packet);
if (!mods.isEmpty()) {
player.setModInfo(new ModInfo("FML", mods));
}
}
// Always forward the FML handshake to the remote server.
backendConn.write(packet);
} else {
// The client is trying to send messages too early. This is primarily caused by mods, but it's further
// aggravated by Velocity. To work around these issues, we will queue any non-FML handshake messages to
// be sent once the JoinGame packet has been received by the proxy.
loginPluginMessages.add(packet);
}
} else {
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
backendConn.write(packet);
} else {
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, packet.getData());
server.getEventManager().fire(event).thenAcceptAsync(pme -> backendConn.write(packet),
backendConn.eventLoop());
}
}
}
return true;
}
@Override
public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.hasCompletedJoin()) {
smc.write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.hasCompletedJoin()) {
smc.write(buf.retain());
}
}
@Override
public void disconnected() {
player.teardown();
}
@Override
public void exception(Throwable throwable) {
player.close(TextComponent.builder()
.content("An exception occurred in your connection: ")
.color(TextColor.RED)
.append(TextComponent.of(ThrowableUtils.briefDescription(throwable), TextColor.WHITE))
.build());
}
@Override
public void writabilityChanged() {
VelocityServerConnection serverConn = player.getConnectedServer();
if (serverConn != null) {
boolean writable = player.getConnection().getChannel().isWritable();
MinecraftConnection smc = serverConn.getConnection();
if (smc != null) {
smc.getChannel().config().setAutoRead(writable);
}
}
}
public void handleBackendJoinGame(JoinGame joinGame) {
VelocityServerConnection serverConn = player.getConnectedServer();
if (serverConn == null) {
throw new IllegalStateException("No server connection for " + player + ", but JoinGame packet received");
}
MinecraftConnection serverMc = serverConn.getConnection();
if (serverMc == null) {
throw new IllegalStateException("Server connection for " + player + " is disconnected, but JoinGame packet received");
}
if (!spawned) {
// Nothing special to do with regards to spawning the player
spawned = true;
player.getConnection().delayedWrite(joinGame);
// We have something special to do for legacy Forge servers - during first connection the FML handshake
// will transition to complete regardless. Thus, we need to ensure that a reset packet is ALWAYS sent on
// first switch.
//
// As we know that calling this branch only happens on first join, we set that if we are a Forge
// client that we must reset on the next switch.
//
// The call will handle if the player is not a Forge player appropriately.
player.getConnection().setCanSendLegacyFMLResetPacket(true);
} else {
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
backendConn.write(packet);
} else {
// Clear tab list to avoid duplicate entries
player.getTabList().clearAll();
// In order to handle switching to another server, you will need to send three packets:
//
// - The join game packet from the backend server
// - A respawn packet with a different dimension
// - Another respawn with the correct dimension
//
// The two respawns with different dimensions are required, otherwise the client gets confused.
//
// Most notably, by having the client accept the join game packet, we can work around the need to perform
// entity ID rewrites, eliminating potential issues from rewriting packets and improving compatibility with
// mods.
player.getConnection().delayedWrite(joinGame);
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
player.getConnection().delayedWrite(new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType()));
player.getConnection().delayedWrite(new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType()));
}
// Remove old boss bars. These don't get cleared when sending JoinGame so we need to track these.
for (UUID serverBossBar : serverBossBars) {
BossBar deletePacket = new BossBar();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(BossBar.REMOVE);
player.getConnection().delayedWrite(deletePacket);
}
serverBossBars.clear();
// Tell the server about this client's plugin message channels.
int serverVersion = serverMc.getProtocolVersion();
Collection<String> toRegister = new HashSet<>(clientPluginMsgChannels);
if (serverVersion >= ProtocolConstants.MINECRAFT_1_13) {
toRegister.addAll(server.getChannelRegistrar().getModernChannelIds());
} else {
toRegister.addAll(server.getChannelRegistrar().getIdsForLegacyConnections());
}
if (!toRegister.isEmpty()) {
serverMc.delayedWrite(PluginMessageUtil.constructChannelsPacket(serverVersion, toRegister));
}
// If we had plugin messages queued during login/FML handshake, send them now.
PluginMessage pm;
while ((pm = loginPluginMessages.poll()) != null) {
serverMc.delayedWrite(pm);
}
// Clear any title from the previous server.
player.getConnection().delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
// Flush everything
player.getConnection().flush();
serverMc.flush();
serverConn.setHasCompletedJoin(true);
if (serverConn.isLegacyForge()) {
// We only need to indicate we can send a reset packet if we complete a handshake, that is,
// logged onto a Forge server.
//
// The special case is if we log onto a Vanilla server as our first server, FML will treat this
// as complete and **will** need a reset packet sending at some point. We will handle this
// during initial player connection if the player is detected to be forge.
//
// This is why we use an if statement rather than the result of VelocityServerConnection#isLegacyForge()
// because we don't want to set it false if this is a first connection to a Vanilla server.
//
// See LoginSessionHandler#handle for where the counterpart to this method is
player.getConnection().setCanSendLegacyFMLResetPacket(true);
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id,
packet.getData());
server.getEventManager().fire(event).thenAcceptAsync(pme -> backendConn.write(packet),
backendConn.eventLoop());
}
}
}
public List<UUID> getServerBossBars() {
return serverBossBars;
return true;
}
@Override
public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
public Set<String> getClientPluginMsgChannels() {
return clientPluginMsgChannels;
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.hasCompletedJoin()) {
smc.write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
public void handleTabCompleteResponse(TabCompleteResponse response) {
if (outstandingTabComplete != null) {
if (!outstandingTabComplete.isAssumeCommand()) {
String command = outstandingTabComplete.getCommand().substring(1);
try {
response.getOffers().addAll(server.getCommandManager().offerSuggestions(player, command));
} catch (Exception e) {
logger.error("Unable to provide tab list completions for {} for command '{}'", player.getUsername(),
command, e);
}
outstandingTabComplete = null;
}
player.getConnection().write(response);
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.hasCompletedJoin()) {
smc.write(buf.retain());
}
}
@Override
public void disconnected() {
player.teardown();
}
@Override
public void exception(Throwable throwable) {
player.close(TextComponent.builder()
.content("An exception occurred in your connection: ")
.color(TextColor.RED)
.append(TextComponent.of(ThrowableUtils.briefDescription(throwable), TextColor.WHITE))
.build());
}
@Override
public void writabilityChanged() {
VelocityServerConnection serverConn = player.getConnectedServer();
if (serverConn != null) {
boolean writable = player.getConnection().getChannel().isWritable();
MinecraftConnection smc = serverConn.getConnection();
if (smc != null) {
smc.getChannel().config().setAutoRead(writable);
}
}
}
public void handleBackendJoinGame(JoinGame joinGame) {
VelocityServerConnection serverConn = player.getConnectedServer();
if (serverConn == null) {
throw new IllegalStateException(
"No server connection for " + player + ", but JoinGame packet received");
}
MinecraftConnection serverMc = serverConn.getConnection();
if (serverMc == null) {
throw new IllegalStateException(
"Server connection for " + player + " is disconnected, but JoinGame packet received");
}
if (!spawned) {
// Nothing special to do with regards to spawning the player
spawned = true;
player.getConnection().delayedWrite(joinGame);
// We have something special to do for legacy Forge servers - during first connection the FML handshake
// will transition to complete regardless. Thus, we need to ensure that a reset packet is ALWAYS sent on
// first switch.
//
// As we know that calling this branch only happens on first join, we set that if we are a Forge
// client that we must reset on the next switch.
//
// The call will handle if the player is not a Forge player appropriately.
player.getConnection().setCanSendLegacyFMLResetPacket(true);
} else {
// Clear tab list to avoid duplicate entries
player.getTabList().clearAll();
// In order to handle switching to another server, you will need to send three packets:
//
// - The join game packet from the backend server
// - A respawn packet with a different dimension
// - Another respawn with the correct dimension
//
// The two respawns with different dimensions are required, otherwise the client gets confused.
//
// Most notably, by having the client accept the join game packet, we can work around the need to perform
// entity ID rewrites, eliminating potential issues from rewriting packets and improving compatibility with
// mods.
player.getConnection().delayedWrite(joinGame);
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
player.getConnection().delayedWrite(
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
player.getConnection().delayedWrite(
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
}
// Remove old boss bars. These don't get cleared when sending JoinGame so we need to track these.
for (UUID serverBossBar : serverBossBars) {
BossBar deletePacket = new BossBar();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(BossBar.REMOVE);
player.getConnection().delayedWrite(deletePacket);
}
serverBossBars.clear();
// Tell the server about this client's plugin message channels.
int serverVersion = serverMc.getProtocolVersion();
Collection<String> toRegister = new HashSet<>(clientPluginMsgChannels);
if (serverVersion >= ProtocolConstants.MINECRAFT_1_13) {
toRegister.addAll(server.getChannelRegistrar().getModernChannelIds());
} else {
toRegister.addAll(server.getChannelRegistrar().getIdsForLegacyConnections());
}
if (!toRegister.isEmpty()) {
serverMc.delayedWrite(PluginMessageUtil.constructChannelsPacket(serverVersion, toRegister));
}
// If we had plugin messages queued during login/FML handshake, send them now.
PluginMessage pm;
while ((pm = loginPluginMessages.poll()) != null) {
serverMc.delayedWrite(pm);
}
// Clear any title from the previous server.
player.getConnection()
.delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
// Flush everything
player.getConnection().flush();
serverMc.flush();
serverConn.setHasCompletedJoin(true);
if (serverConn.isLegacyForge()) {
// We only need to indicate we can send a reset packet if we complete a handshake, that is,
// logged onto a Forge server.
//
// The special case is if we log onto a Vanilla server as our first server, FML will treat this
// as complete and **will** need a reset packet sending at some point. We will handle this
// during initial player connection if the player is detected to be forge.
//
// This is why we use an if statement rather than the result of VelocityServerConnection#isLegacyForge()
// because we don't want to set it false if this is a first connection to a Vanilla server.
//
// See LoginSessionHandler#handle for where the counterpart to this method is
player.getConnection().setCanSendLegacyFMLResetPacket(true);
}
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
public Set<String> getClientPluginMsgChannels() {
return clientPluginMsgChannels;
}
public void handleTabCompleteResponse(TabCompleteResponse response) {
if (outstandingTabComplete != null) {
if (!outstandingTabComplete.isAssumeCommand()) {
String command = outstandingTabComplete.getCommand().substring(1);
try {
response.getOffers().addAll(server.getCommandManager().offerSuggestions(player, command));
} catch (Exception e) {
logger.error("Unable to provide tab list completions for {} for command '{}'",
player.getUsername(),
command, e);
}
outstandingTabComplete = null;
}
player.getConnection().write(response);
}
}
}

View File

@@ -3,58 +3,59 @@ package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.proxy.player.PlayerSettings;
import com.velocitypowered.api.proxy.player.SkinParts;
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
import java.util.Locale;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Locale;
public class ClientSettingsWrapper implements PlayerSettings {
static final PlayerSettings DEFAULT = new ClientSettingsWrapper(new ClientSettings("en_US", (byte) 10, 0, true, (short)127, 1));
private final ClientSettings settings;
private final SkinParts parts;
private @Nullable Locale locale;
ClientSettingsWrapper(ClientSettings settings) {
this.settings = settings;
this.parts = new SkinParts((byte) settings.getSkinParts());
}
static final PlayerSettings DEFAULT = new ClientSettingsWrapper(
new ClientSettings("en_US", (byte) 10, 0, true, (short) 127, 1));
@Override
public Locale getLocale() {
if (locale == null) {
locale = Locale.forLanguageTag(settings.getLocale().replaceAll("_", "-"));
}
return locale;
}
private final ClientSettings settings;
private final SkinParts parts;
private @Nullable Locale locale;
@Override
public byte getViewDistance() {
return settings.getViewDistance();
}
ClientSettingsWrapper(ClientSettings settings) {
this.settings = settings;
this.parts = new SkinParts((byte) settings.getSkinParts());
}
@Override
public ChatMode getChatMode() {
int chat = settings.getChatVisibility();
if (chat < 0 || chat > 2) {
return ChatMode.SHOWN;
}
return ChatMode.values()[chat];
@Override
public Locale getLocale() {
if (locale == null) {
locale = Locale.forLanguageTag(settings.getLocale().replaceAll("_", "-"));
}
return locale;
}
@Override
public boolean hasChatColors() {
return settings.isChatColors();
}
@Override
public byte getViewDistance() {
return settings.getViewDistance();
}
@Override
public SkinParts getSkinParts() {
return parts;
@Override
public ChatMode getChatMode() {
int chat = settings.getChatVisibility();
if (chat < 0 || chat > 2) {
return ChatMode.SHOWN;
}
return ChatMode.values()[chat];
}
@Override
public boolean hasChatColors() {
return settings.isChatColors();
}
@Override
public SkinParts getSkinParts() {
return parts;
}
@Override
public MainHand getMainHand() {
return settings.getMainHand() == 1 ? MainHand.RIGHT : MainHand.LEFT;
}
@Override
public MainHand getMainHand() {
return settings.getMainHand() == 1 ? MainHand.RIGHT : MainHand.LEFT;
}
}

View File

@@ -14,144 +14,159 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import com.velocitypowered.proxy.protocol.packet.LegacyDisconnect;
import com.velocitypowered.proxy.protocol.packet.LegacyHandshake;
import com.velocitypowered.proxy.protocol.packet.LegacyPing;
import com.velocitypowered.proxy.protocol.packet.LegacyPingResponse;
import io.netty.buffer.ByteBuf;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Optional;
import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
import net.kyori.text.format.TextColor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Optional;
public class HandshakeSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection;
private final VelocityServer server;
public HandshakeSessionHandler(MinecraftConnection connection, VelocityServer server) {
this.connection = Preconditions.checkNotNull(connection, "connection");
this.server = Preconditions.checkNotNull(server, "server");
}
@Override
public boolean handle(LegacyPing packet) {
connection.setProtocolVersion(ProtocolConstants.LEGACY);
VelocityConfiguration configuration = server.getConfiguration();
ServerPing ping = new ServerPing(
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION,
"Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
ImmutableList.of()),
configuration.getMotdComponent(),
null,
null
);
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);
server.getEventManager().fire(event)
.thenRunAsync(() -> {
// The disconnect packet is the same as the server response one.
LegacyPingResponse response = LegacyPingResponse.from(event.getPing());
connection.closeWith(LegacyDisconnect.fromPingResponse(response));
}, connection.eventLoop());
return true;
}
@Override
public boolean handle(LegacyHandshake packet) {
connection.closeWith(LegacyDisconnect
.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED)));
return true;
}
@Override
public boolean handle(Handshake handshake) {
InitialInboundConnection ic = new InitialInboundConnection(connection,
cleanVhost(handshake.getServerAddress()), handshake);
switch (handshake.getNextStatus()) {
case StateRegistry.STATUS_ID:
connection.setState(StateRegistry.STATUS);
connection.setProtocolVersion(handshake.getProtocolVersion());
connection.setSessionHandler(new StatusSessionHandler(server, connection, ic));
return true;
case StateRegistry.LOGIN_ID:
connection.setState(StateRegistry.LOGIN);
connection.setProtocolVersion(handshake.getProtocolVersion());
if (!ProtocolConstants.isSupported(handshake.getProtocolVersion())) {
connection.closeWith(Disconnect
.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
return true;
}
InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress();
if (!server.getIpAttemptLimiter().attempt(address)) {
connection.closeWith(
Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
return true;
}
// Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13) and store that in the connection
boolean isForge = handshake.getServerAddress().endsWith("\0FML\0");
connection.setLegacyForge(isForge);
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ reject Forge
if (handshake.getServerAddress().contains("\0") && !isForge) {
connection.closeWith(Disconnect
.create(TextComponent.of("Running Velocity behind Velocity is unsupported.")));
return true;
}
// If the proxy is configured for modern forwarding, we must deny connections from 1.12.2 and lower,
// otherwise IP information will never get forwarded.
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN
&& handshake.getProtocolVersion() <
ProtocolConstants.MINECRAFT_1_13) {
connection.closeWith(Disconnect
.create(TextComponent.of("This server is only compatible with 1.13 and above.")));
return true;
}
server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
connection.setSessionHandler(new LoginSessionHandler(server, connection, ic));
return true;
default:
throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus());
}
}
private String cleanVhost(String hostname) {
int zeroIdx = hostname.indexOf('\0');
return zeroIdx == -1 ? hostname : hostname.substring(0, zeroIdx);
}
@Override
public void handleGeneric(MinecraftPacket packet) {
// Unknown packet received. Better to close the connection.
connection.close();
}
@Override
public void handleUnknown(ByteBuf buf) {
// Unknown packet received. Better to close the connection.
connection.close();
}
private static class LegacyInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private final VelocityServer server;
public HandshakeSessionHandler(MinecraftConnection connection, VelocityServer server) {
this.connection = Preconditions.checkNotNull(connection, "connection");
this.server = Preconditions.checkNotNull(server, "server");
private LegacyInboundConnection(MinecraftConnection connection) {
this.connection = connection;
}
@Override
public boolean handle(LegacyPing packet) {
connection.setProtocolVersion(ProtocolConstants.LEGACY);
VelocityConfiguration configuration = server.getConfiguration();
ServerPing ping = new ServerPing(
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
null,
null
);
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);
server.getEventManager().fire(event)
.thenRunAsync(() -> {
// The disconnect packet is the same as the server response one.
LegacyPingResponse response = LegacyPingResponse.from(event.getPing());
connection.closeWith(LegacyDisconnect.fromPingResponse(response));
}, connection.eventLoop());
return true;
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getRemoteAddress();
}
@Override
public boolean handle(LegacyHandshake packet) {
connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED)));
return true;
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.empty();
}
@Override
public boolean handle(Handshake handshake) {
InitialInboundConnection ic = new InitialInboundConnection(connection, cleanVhost(handshake.getServerAddress()), handshake);
switch (handshake.getNextStatus()) {
case StateRegistry.STATUS_ID:
connection.setState(StateRegistry.STATUS);
connection.setProtocolVersion(handshake.getProtocolVersion());
connection.setSessionHandler(new StatusSessionHandler(server, connection, ic));
return true;
case StateRegistry.LOGIN_ID:
connection.setState(StateRegistry.LOGIN);
connection.setProtocolVersion(handshake.getProtocolVersion());
if (!ProtocolConstants.isSupported(handshake.getProtocolVersion())) {
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
return true;
}
InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress();
if (!server.getIpAttemptLimiter().attempt(address)) {
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
return true;
}
// Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13) and store that in the connection
boolean isForge = handshake.getServerAddress().endsWith("\0FML\0");
connection.setLegacyForge(isForge);
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ reject Forge
if (handshake.getServerAddress().contains("\0") && !isForge) {
connection.closeWith(Disconnect.create(TextComponent.of("Running Velocity behind Velocity is unsupported.")));
return true;
}
// If the proxy is configured for modern forwarding, we must deny connections from 1.12.2 and lower,
// otherwise IP information will never get forwarded.
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && handshake.getProtocolVersion() <
ProtocolConstants.MINECRAFT_1_13) {
connection.closeWith(Disconnect.create(TextComponent.of("This server is only compatible with 1.13 and above.")));
return true;
}
server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
connection.setSessionHandler(new LoginSessionHandler(server, connection, ic));
return true;
default:
throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus());
}
}
private String cleanVhost(String hostname) {
int zeroIdx = hostname.indexOf('\0');
return zeroIdx == -1 ? hostname : hostname.substring(0, zeroIdx);
public boolean isActive() {
return !connection.isClosed();
}
@Override
public void handleGeneric(MinecraftPacket packet) {
// Unknown packet received. Better to close the connection.
connection.close();
}
@Override
public void handleUnknown(ByteBuf buf) {
// Unknown packet received. Better to close the connection.
connection.close();
}
private static class LegacyInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private LegacyInboundConnection(MinecraftConnection connection) {
this.connection = connection;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getRemoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.empty();
}
@Override
public boolean isActive() {
return !connection.isClosed();
}
@Override
public int getProtocolVersion() {
return 0;
}
public int getProtocolVersion() {
return 0;
}
}
}

View File

@@ -4,20 +4,21 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
public class InitialConnectSessionHandler implements MinecraftSessionHandler {
private final ConnectedPlayer player;
InitialConnectSessionHandler(ConnectedPlayer player) {
this.player = player;
}
private final ConnectedPlayer player;
@Override
public void handleGeneric(MinecraftPacket packet) {
// No-op: will never handle packets
}
InitialConnectSessionHandler(ConnectedPlayer player) {
this.player = player;
}
@Override
public void disconnected() {
// the user cancelled the login process
player.teardown();
}
@Override
public void handleGeneric(MinecraftPacket packet) {
// No-op: will never handle packets
}
@Override
public void disconnected() {
// the user cancelled the login process
player.teardown();
}
}

View File

@@ -3,38 +3,39 @@ package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import java.net.InetSocketAddress;
import java.util.Optional;
class InitialInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private final String cleanedAddress;
private final Handshake handshake;
InitialInboundConnection(MinecraftConnection connection, String cleanedAddress, Handshake handshake) {
this.connection = connection;
this.cleanedAddress = cleanedAddress;
this.handshake = handshake;
}
private final MinecraftConnection connection;
private final String cleanedAddress;
private final Handshake handshake;
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getRemoteAddress();
}
InitialInboundConnection(MinecraftConnection connection, String cleanedAddress,
Handshake handshake) {
this.connection = connection;
this.cleanedAddress = cleanedAddress;
this.handshake = handshake;
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.of(InetSocketAddress.createUnresolved(cleanedAddress, handshake.getPort()));
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getRemoteAddress();
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.of(InetSocketAddress.createUnresolved(cleanedAddress, handshake.getPort()));
}
@Override
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
}
@Override
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
}

View File

@@ -1,5 +1,7 @@
package com.velocitypowered.proxy.connection.client;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
@@ -17,18 +19,18 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequest;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponse;
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage;
import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse;
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess;
import com.velocitypowered.proxy.protocol.packet.SetCompression;
import com.velocitypowered.proxy.util.EncryptionUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
@@ -39,246 +41,266 @@ import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public class LoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class);
private static final String MOJANG_SERVER_AUTH_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class);
private static final String MOJANG_SERVER_AUTH_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
private final VelocityServer server;
private final MinecraftConnection inbound;
private final InboundConnection apiInbound;
private @MonotonicNonNull ServerLogin login;
private byte[] verify = EMPTY_BYTE_ARRAY;
private int playerInfoId;
private @MonotonicNonNull ConnectedPlayer connectedPlayer;
private final VelocityServer server;
private final MinecraftConnection inbound;
private final InboundConnection apiInbound;
private @MonotonicNonNull ServerLogin login;
private byte[] verify = EMPTY_BYTE_ARRAY;
private int playerInfoId;
private @MonotonicNonNull ConnectedPlayer connectedPlayer;
public LoginSessionHandler(VelocityServer server, MinecraftConnection inbound, InboundConnection apiInbound) {
this.server = Preconditions.checkNotNull(server, "server");
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
this.apiInbound = Preconditions.checkNotNull(apiInbound, "apiInbound");
public LoginSessionHandler(VelocityServer server, MinecraftConnection inbound,
InboundConnection apiInbound) {
this.server = Preconditions.checkNotNull(server, "server");
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
this.apiInbound = Preconditions.checkNotNull(apiInbound, "apiInbound");
}
@Override
public boolean handle(ServerLogin packet) {
this.login = packet;
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
playerInfoId = ThreadLocalRandom.current().nextInt();
inbound.write(
new LoginPluginMessage(playerInfoId, VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL,
Unpooled.EMPTY_BUFFER));
} else {
beginPreLogin();
}
return true;
}
@Override
public boolean handle(LoginPluginResponse packet) {
if (packet.getId() == playerInfoId) {
if (packet.isSuccess()) {
// Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening.
inbound.closeWith(Disconnect.create(
TextComponent.of("Running Velocity behind Velocity isn't supported.", TextColor.RED)
));
} else {
// Proceed with the regular login process.
beginPreLogin();
}
}
return true;
}
@Override
public boolean handle(EncryptionResponse packet) {
ServerLogin login = this.login;
if (login == null) {
throw new IllegalStateException("No ServerLogin packet received yet.");
}
@Override
public boolean handle(ServerLogin packet) {
this.login = packet;
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
playerInfoId = ThreadLocalRandom.current().nextInt();
inbound.write(new LoginPluginMessage(playerInfoId, VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL,
Unpooled.EMPTY_BUFFER));
} else {
beginPreLogin();
}
return true;
if (verify.length == 0) {
throw new IllegalStateException("No EncryptionRequest packet sent yet.");
}
@Override
public boolean handle(LoginPluginResponse packet) {
if (packet.getId() == playerInfoId) {
if (packet.isSuccess()) {
// Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening.
inbound.closeWith(Disconnect.create(
TextComponent.of("Running Velocity behind Velocity isn't supported.", TextColor.RED)
));
try {
KeyPair serverKeyPair = server.getServerKeyPair();
byte[] decryptedVerifyToken = EncryptionUtils
.decryptRsa(serverKeyPair, packet.getVerifyToken());
if (!Arrays.equals(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
}
byte[] decryptedSharedSecret = EncryptionUtils
.decryptRsa(serverKeyPair, packet.getSharedSecret());
String serverId = EncryptionUtils
.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) inbound.getRemoteAddress()).getHostString();
server.getHttpClient()
.get(new URL(
String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
.thenAcceptAsync(profileResponse -> {
if (inbound.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption is
// enabled.
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
if (profileResponse.getCode() == 200) {
// All went well, initialize the session.
initializePlayer(
VelocityServer.GSON.fromJson(profileResponse.getBody(), GameProfile.class), true);
} else if (profileResponse.getCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy. The client has enabled
// encryption, so we need to do that as well.
logger.warn("An offline-mode client ({} from {}) tried to connect!",
login.getUsername(), playerIp);
inbound.closeWith(Disconnect.create(TextComponent
.of("This server only accepts connections from online-mode clients.")));
} else {
// Proceed with the regular login process.
beginPreLogin();
// Something else went wrong
logger.error(
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
profileResponse.getCode(), login.getUsername(), playerIp);
inbound.close();
}
}
return true;
}
@Override
public boolean handle(EncryptionResponse packet) {
ServerLogin login = this.login;
if (login == null) {
throw new IllegalStateException("No ServerLogin packet received yet.");
}
if (verify.length == 0) {
throw new IllegalStateException("No EncryptionRequest packet sent yet.");
}
try {
KeyPair serverKeyPair = server.getServerKeyPair();
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, packet.getVerifyToken());
if (!Arrays.equals(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
}
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, packet.getSharedSecret());
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) inbound.getRemoteAddress()).getHostString();
server.getHttpClient()
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
.thenAcceptAsync(profileResponse -> {
if (inbound.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption is
// enabled.
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
if (profileResponse.getCode() == 200) {
// All went well, initialize the session.
initializePlayer(VelocityServer.GSON.fromJson(profileResponse.getBody(), GameProfile.class), true);
} else if (profileResponse.getCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy. The client has enabled
// encryption, so we need to do that as well.
logger.warn("An offline-mode client ({} from {}) tried to connect!", login.getUsername(), playerIp);
inbound.closeWith(Disconnect.create(TextComponent.of("This server only accepts connections from online-mode clients.")));
} else {
// Something else went wrong
logger.error("Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
profileResponse.getCode(), login.getUsername(), playerIp);
inbound.close();
}
}, inbound.eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
inbound.close();
return null;
});
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e);
}, inbound.eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
inbound.close();
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
return true;
return null;
});
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e);
inbound.close();
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
return true;
}
private void beginPreLogin() {
ServerLogin login = this.login;
if (login == null) {
throw new IllegalStateException("No ServerLogin packet received yet.");
}
PreLoginEvent event = new PreLoginEvent(apiInbound, login.getUsername());
server.getEventManager().fire(event)
.thenRunAsync(() -> {
if (inbound.isClosed()) {
// The player was disconnected
return;
}
PreLoginComponentResult result = event.getResult();
Optional<Component> disconnectReason = result.getReason();
if (disconnectReason.isPresent()) {
// The component is guaranteed to be provided if the connection was denied.
inbound.closeWith(Disconnect.create(disconnectReason.get()));
return;
}
if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed())) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
} else {
initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()), false);
}
}, inbound.eventLoop());
private void beginPreLogin() {
ServerLogin login = this.login;
if (login == null) {
throw new IllegalStateException("No ServerLogin packet received yet.");
}
private EncryptionRequest generateRequest() {
byte[] verify = new byte[4];
ThreadLocalRandom.current().nextBytes(verify);
EncryptionRequest request = new EncryptionRequest();
request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded());
request.setVerifyToken(verify);
return request;
}
private void initializePlayer(GameProfile profile, boolean onlineMode) {
if (inbound.isLegacyForge() && server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) {
// We want to add the FML token to the properties
List<GameProfile.Property> properties = new ArrayList<>(profile.getProperties());
properties.add(new GameProfile.Property("forgeClient", "true", ""));
profile = new GameProfile(profile.getId(), profile.getName(), properties);
}
GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, onlineMode);
server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(server, profileEvent.getGameProfile(), inbound,
apiInbound.getVirtualHost().orElse(null));
this.connectedPlayer = player;
return server.getEventManager().fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS))
.thenCompose(event -> {
// wait for permissions to load, then set the players permission function
player.setPermissionFunction(event.createFunction(player));
// then call & wait for the login event
return server.getEventManager().fire(new LoginEvent(player));
})
// then complete the connection
.thenAcceptAsync(event -> {
if (inbound.isClosed()) {
// The player was disconnected
return;
}
Optional<Component> reason = event.getResult().getReason();
if (reason.isPresent()) {
player.disconnect(reason.get());
} else {
handleProxyLogin(player);
}
}, inbound.eventLoop());
});
}
private void handleProxyLogin(ConnectedPlayer player) {
Optional<RegisteredServer> toTry = player.getNextServerToTry();
if (!toTry.isPresent()) {
player.close(TextComponent.of("No available servers", TextColor.RED));
PreLoginEvent event = new PreLoginEvent(apiInbound, login.getUsername());
server.getEventManager().fire(event)
.thenRunAsync(() -> {
if (inbound.isClosed()) {
// The player was disconnected
return;
}
if (!server.registerConnection(player)) {
inbound.closeWith(Disconnect.create(TextComponent.of("You are already on this proxy!", TextColor.RED)));
}
PreLoginComponentResult result = event.getResult();
Optional<Component> disconnectReason = result.getReason();
if (disconnectReason.isPresent()) {
// The component is guaranteed to be provided if the connection was denied.
inbound.closeWith(Disconnect.create(disconnectReason.get()));
return;
}
}
int threshold = server.getConfiguration().getCompressionThreshold();
if (threshold >= 0) {
inbound.write(new SetCompression(threshold));
inbound.setCompressionThreshold(threshold);
}
if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() || result
.isOnlineModeAllowed())) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
} else {
initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()), false);
}
}, inbound.eventLoop());
}
ServerLoginSuccess success = new ServerLoginSuccess();
success.setUsername(player.getUsername());
success.setUuid(player.getUniqueId());
inbound.write(success);
private EncryptionRequest generateRequest() {
byte[] verify = new byte[4];
ThreadLocalRandom.current().nextBytes(verify);
inbound.setAssociation(player);
inbound.setState(StateRegistry.PLAY);
EncryptionRequest request = new EncryptionRequest();
request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded());
request.setVerifyToken(verify);
return request;
}
logger.info("{} has connected", player);
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
server.getEventManager().fire(new PostLoginEvent(player)).thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());
private void initializePlayer(GameProfile profile, boolean onlineMode) {
if (inbound.isLegacyForge()
&& server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) {
// We want to add the FML token to the properties
List<GameProfile.Property> properties = new ArrayList<>(profile.getProperties());
properties.add(new GameProfile.Property("forgeClient", "true", ""));
profile = new GameProfile(profile.getId(), profile.getName(), properties);
}
GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile,
onlineMode);
server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(server, profileEvent.getGameProfile(), inbound,
apiInbound.getVirtualHost().orElse(null));
this.connectedPlayer = player;
return server.getEventManager()
.fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS))
.thenCompose(event -> {
// wait for permissions to load, then set the players permission function
player.setPermissionFunction(event.createFunction(player));
// then call & wait for the login event
return server.getEventManager().fire(new LoginEvent(player));
})
// then complete the connection
.thenAcceptAsync(event -> {
if (inbound.isClosed()) {
// The player was disconnected
return;
}
Optional<Component> reason = event.getResult().getReason();
if (reason.isPresent()) {
player.disconnect(reason.get());
} else {
handleProxyLogin(player);
}
}, inbound.eventLoop());
});
}
private void handleProxyLogin(ConnectedPlayer player) {
Optional<RegisteredServer> toTry = player.getNextServerToTry();
if (!toTry.isPresent()) {
player.close(TextComponent.of("No available servers", TextColor.RED));
return;
}
@Override
public void handleUnknown(ByteBuf buf) {
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
if (!server.registerConnection(player)) {
inbound.closeWith(
Disconnect.create(TextComponent.of("You are already on this proxy!", TextColor.RED)));
return;
}
@Override
public void disconnected() {
if (connectedPlayer != null) {
connectedPlayer.teardown();
}
int threshold = server.getConfiguration().getCompressionThreshold();
if (threshold >= 0) {
inbound.write(new SetCompression(threshold));
inbound.setCompressionThreshold(threshold);
}
ServerLoginSuccess success = new ServerLoginSuccess();
success.setUsername(player.getUsername());
success.setUuid(player.getUniqueId());
inbound.write(success);
inbound.setAssociation(player);
inbound.setState(StateRegistry.PLAY);
logger.info("{} has connected", player);
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
server.getEventManager().fire(new PostLoginEvent(player))
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());
}
@Override
public void handleUnknown(ByteBuf buf) {
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
}
@Override
public void disconnected() {
if (connectedPlayer != null) {
connectedPlayer.teardown();
}
}
}

View File

@@ -16,46 +16,52 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponse;
import io.netty.buffer.ByteBuf;
public class StatusSessionHandler implements MinecraftSessionHandler {
private final VelocityServer server;
private final MinecraftConnection connection;
private final InboundConnection inboundWrapper;
StatusSessionHandler(VelocityServer server, MinecraftConnection connection, InboundConnection inboundWrapper) {
this.server = server;
this.connection = connection;
this.inboundWrapper = inboundWrapper;
}
private final VelocityServer server;
private final MinecraftConnection connection;
private final InboundConnection inboundWrapper;
@Override
public boolean handle(StatusPing packet) {
connection.closeWith(packet);
return true;
}
StatusSessionHandler(VelocityServer server, MinecraftConnection connection,
InboundConnection inboundWrapper) {
this.server = server;
this.connection = connection;
this.inboundWrapper = inboundWrapper;
}
@Override
public boolean handle(StatusRequest packet) {
VelocityConfiguration configuration = server.getConfiguration();
@Override
public boolean handle(StatusPing packet) {
connection.closeWith(packet);
return true;
}
int shownVersion = ProtocolConstants.isSupported(connection.getProtocolVersion()) ? connection.getProtocolVersion() :
ProtocolConstants.MAXIMUM_GENERIC_VERSION;
ServerPing initialPing = new ServerPing(
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
configuration.getFavicon().orElse(null),
configuration.isAnnounceForge() ? ModInfo.DEFAULT : null
);
@Override
public boolean handle(StatusRequest packet) {
VelocityConfiguration configuration = server.getConfiguration();
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
server.getEventManager().fire(event)
.thenRunAsync(() -> connection.write(new StatusResponse(VelocityServer.GSON.toJson(event.getPing()))),
connection.eventLoop());
return true;
}
int shownVersion = ProtocolConstants.isSupported(connection.getProtocolVersion()) ? connection
.getProtocolVersion() :
ProtocolConstants.MAXIMUM_GENERIC_VERSION;
ServerPing initialPing = new ServerPing(
new ServerPing.Version(shownVersion,
"Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
ImmutableList.of()),
configuration.getMotdComponent(),
configuration.getFavicon().orElse(null),
configuration.isAnnounceForge() ? ModInfo.DEFAULT : null
);
@Override
public void handleUnknown(ByteBuf buf) {
// what even is going on?
connection.close();
}
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
server.getEventManager().fire(event)
.thenRunAsync(
() -> connection.write(new StatusResponse(VelocityServer.GSON.toJson(event.getPing()))),
connection.eventLoop());
return true;
}
@Override
public void handleUnknown(ByteBuf buf) {
// what even is going on?
connection.close();
}
}

View File

@@ -3,19 +3,20 @@ package com.velocitypowered.proxy.connection.forge;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
public class ForgeConstants {
public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS";
public static final String FORGE_LEGACY_CHANNEL = "FML";
public static final String FORGE_MULTIPART_LEGACY_CHANNEL = "FML|MP";
private static final byte[] FORGE_LEGACY_HANDSHAKE_RESET_DATA = new byte[] { -2, 0 };
private ForgeConstants() {
throw new AssertionError();
}
public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS";
public static final String FORGE_LEGACY_CHANNEL = "FML";
public static final String FORGE_MULTIPART_LEGACY_CHANNEL = "FML|MP";
private static final byte[] FORGE_LEGACY_HANDSHAKE_RESET_DATA = new byte[]{-2, 0};
public static PluginMessage resetPacket() {
PluginMessage msg = new PluginMessage();
msg.setChannel(FORGE_LEGACY_HANDSHAKE_CHANNEL);
msg.setData(FORGE_LEGACY_HANDSHAKE_RESET_DATA.clone());
return msg;
}
private ForgeConstants() {
throw new AssertionError();
}
public static PluginMessage resetPacket() {
PluginMessage msg = new PluginMessage();
msg.setChannel(FORGE_LEGACY_HANDSHAKE_CHANNEL);
msg.setData(FORGE_LEGACY_HANDSHAKE_RESET_DATA.clone());
return msg;
}
}

View File

@@ -7,39 +7,40 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.util.List;
public class ForgeUtil {
private ForgeUtil() {
throw new AssertionError();
}
public static List<ModInfo.Mod> readModList(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkArgument(message.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL),
"message is not a FML HS plugin message");
private ForgeUtil() {
throw new AssertionError();
}
ByteBuf byteBuf = Unpooled.wrappedBuffer(message.getData());
try {
byte discriminator = byteBuf.readByte();
public static List<ModInfo.Mod> readModList(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
Preconditions
.checkArgument(message.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL),
"message is not a FML HS plugin message");
if (discriminator == 2) {
ImmutableList.Builder<ModInfo.Mod> mods = ImmutableList.builder();
int modCount = ProtocolUtils.readVarInt(byteBuf);
ByteBuf byteBuf = Unpooled.wrappedBuffer(message.getData());
try {
byte discriminator = byteBuf.readByte();
for (int index = 0; index < modCount; index++) {
String id = ProtocolUtils.readString(byteBuf);
String version = ProtocolUtils.readString(byteBuf);
mods.add(new ModInfo.Mod(id, version));
}
if (discriminator == 2) {
ImmutableList.Builder<ModInfo.Mod> mods = ImmutableList.builder();
int modCount = ProtocolUtils.readVarInt(byteBuf);
return mods.build();
}
return ImmutableList.of();
} finally {
byteBuf.release();
for (int index = 0; index < modCount; index++) {
String id = ProtocolUtils.readString(byteBuf);
String version = ProtocolUtils.readString(byteBuf);
mods.add(new ModInfo.Mod(id, version));
}
return mods.build();
}
return ImmutableList.of();
} finally {
byteBuf.release();
}
}
}

View File

@@ -4,12 +4,17 @@ import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
public class ConnectionMessages {
public static final TextComponent ALREADY_CONNECTED = TextComponent.of("You are already connected to this server!", TextColor.RED);
public static final TextComponent IN_PROGRESS = TextComponent.of("You are already connecting to a server!", TextColor.RED);
public static final TextComponent INTERNAL_SERVER_CONNECTION_ERROR = TextComponent.of("Internal server connection error");
public static final TextComponent UNEXPECTED_DISCONNECT = TextComponent.of("Unexpectedly disconnected from server - crash?");
private ConnectionMessages() {
throw new AssertionError();
}
public static final TextComponent ALREADY_CONNECTED = TextComponent
.of("You are already connected to this server!", TextColor.RED);
public static final TextComponent IN_PROGRESS = TextComponent
.of("You are already connecting to a server!", TextColor.RED);
public static final TextComponent INTERNAL_SERVER_CONNECTION_ERROR = TextComponent
.of("Internal server connection error");
public static final TextComponent UNEXPECTED_DISCONNECT = TextComponent
.of("Unexpectedly disconnected from server - crash?");
private ConnectionMessages() {
throw new AssertionError();
}
}

View File

@@ -2,48 +2,50 @@ package com.velocitypowered.proxy.connection.util;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import java.util.Optional;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import java.util.Optional;
public class ConnectionRequestResults {
public static final ConnectionRequestBuilder.Result SUCCESSFUL = plainResult(ConnectionRequestBuilder.Status.SUCCESS);
private ConnectionRequestResults() {
throw new AssertionError();
}
public static final ConnectionRequestBuilder.Result SUCCESSFUL = plainResult(
ConnectionRequestBuilder.Status.SUCCESS);
public static ConnectionRequestBuilder.Result plainResult(ConnectionRequestBuilder.Status status) {
return new ConnectionRequestBuilder.Result() {
@Override
public ConnectionRequestBuilder.Status getStatus() {
return status;
}
private ConnectionRequestResults() {
throw new AssertionError();
}
@Override
public Optional<Component> getReason() {
return Optional.empty();
}
};
}
public static ConnectionRequestBuilder.Result plainResult(
ConnectionRequestBuilder.Status status) {
return new ConnectionRequestBuilder.Result() {
@Override
public ConnectionRequestBuilder.Status getStatus() {
return status;
}
public static ConnectionRequestBuilder.Result forDisconnect(Disconnect disconnect) {
Component deserialized = ComponentSerializers.JSON.deserialize(disconnect.getReason());
return forDisconnect(deserialized);
}
@Override
public Optional<Component> getReason() {
return Optional.empty();
}
};
}
public static ConnectionRequestBuilder.Result forDisconnect(Component component) {
return new ConnectionRequestBuilder.Result() {
@Override
public ConnectionRequestBuilder.Status getStatus() {
return ConnectionRequestBuilder.Status.SERVER_DISCONNECTED;
}
public static ConnectionRequestBuilder.Result forDisconnect(Disconnect disconnect) {
Component deserialized = ComponentSerializers.JSON.deserialize(disconnect.getReason());
return forDisconnect(deserialized);
}
@Override
public Optional<Component> getReason() {
return Optional.of(component);
}
};
}
public static ConnectionRequestBuilder.Result forDisconnect(Component component) {
return new ConnectionRequestBuilder.Result() {
@Override
public ConnectionRequestBuilder.Status getStatus() {
return ConnectionRequestBuilder.Status.SERVER_DISCONNECTED;
}
@Override
public Optional<Component> getReason() {
return Optional.of(component);
}
};
}
}

View File

@@ -5,6 +5,7 @@ import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
import com.velocitypowered.api.permission.PermissionFunction;
import com.velocitypowered.api.permission.Tristate;
import com.velocitypowered.proxy.VelocityServer;
import java.util.List;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
@@ -17,75 +18,77 @@ import org.jline.reader.Candidate;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import java.util.List;
public final class VelocityConsole extends SimpleTerminalConsole implements CommandSource {
private static final Logger logger = LogManager.getLogger(VelocityConsole.class);
private final VelocityServer server;
private PermissionFunction permissionFunction = PermissionFunction.ALWAYS_TRUE;
private static final Logger logger = LogManager.getLogger(VelocityConsole.class);
public VelocityConsole(VelocityServer server) {
this.server = server;
}
private final VelocityServer server;
private PermissionFunction permissionFunction = PermissionFunction.ALWAYS_TRUE;
@Override
public void sendMessage(Component component) {
logger.info(ComponentSerializers.LEGACY.serialize(component));
}
public VelocityConsole(VelocityServer server) {
this.server = server;
}
@Override
public @NonNull Tristate getPermissionValue(@NonNull String permission) {
return this.permissionFunction.getPermissionValue(permission);
}
@Override
public void sendMessage(Component component) {
logger.info(ComponentSerializers.LEGACY.serialize(component));
}
public void setupPermissions() {
PermissionsSetupEvent event = new PermissionsSetupEvent(this, s -> PermissionFunction.ALWAYS_TRUE);
this.server.getEventManager().fire(event).join(); // this is called on startup, we can safely #join
this.permissionFunction = event.createFunction(this);
}
@Override
public @NonNull Tristate getPermissionValue(@NonNull String permission) {
return this.permissionFunction.getPermissionValue(permission);
}
@Override
protected LineReader buildReader(LineReaderBuilder builder) {
return super.buildReader(builder
.appName("Velocity")
.completer((reader, parsedLine, list) -> {
try {
boolean isCommand = parsedLine.line().indexOf(' ') == -1;
List<String> offers = this.server.getCommandManager().offerSuggestions(this, parsedLine.line());
for (String offer : offers) {
if (isCommand) {
list.add(new Candidate(offer.substring(1)));
} else {
list.add(new Candidate(offer));
}
}
} catch (Exception e) {
logger.error("An error occurred while trying to perform tab completion.", e);
}
})
);
}
public void setupPermissions() {
PermissionsSetupEvent event = new PermissionsSetupEvent(this,
s -> PermissionFunction.ALWAYS_TRUE);
this.server.getEventManager().fire(event)
.join(); // this is called on startup, we can safely #join
this.permissionFunction = event.createFunction(this);
}
@Override
protected boolean isRunning() {
return !this.server.isShutdown();
}
@Override
protected void runCommand(String command) {
try {
if (!this.server.getCommandManager().execute(this, command)) {
sendMessage(TextComponent.of("Command not found.", TextColor.RED));
@Override
protected LineReader buildReader(LineReaderBuilder builder) {
return super.buildReader(builder
.appName("Velocity")
.completer((reader, parsedLine, list) -> {
try {
boolean isCommand = parsedLine.line().indexOf(' ') == -1;
List<String> offers = this.server.getCommandManager()
.offerSuggestions(this, parsedLine.line());
for (String offer : offers) {
if (isCommand) {
list.add(new Candidate(offer.substring(1)));
} else {
list.add(new Candidate(offer));
}
}
} catch (Exception e) {
logger.error("An error occurred while running this command.", e);
}
}
} catch (Exception e) {
logger.error("An error occurred while trying to perform tab completion.", e);
}
})
);
}
@Override
protected void shutdown() {
this.server.shutdown();
@Override
protected boolean isRunning() {
return !this.server.isShutdown();
}
@Override
protected void runCommand(String command) {
try {
if (!this.server.getCommandManager().execute(this, command)) {
sendMessage(TextComponent.of("Command not found.", TextColor.RED));
}
} catch (Exception e) {
logger.error("An error occurred while running this command.", e);
}
}
@Override
protected void shutdown() {
this.server.shutdown();
}
}

View File

@@ -10,95 +10,99 @@ import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.WriteBufferWaterMark;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public final class ConnectionManager {
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 16, 1 << 18);
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
private final Set<Channel> endpoints = new HashSet<>();
private final TransportType transportType;
private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup;
private final VelocityServer server;
public final ServerChannelInitializerHolder serverChannelInitializer;
public ConnectionManager(VelocityServer server) {
this.server = server;
this.transportType = TransportType.bestType();
this.bossGroup = this.transportType.createEventLoopGroup(TransportType.Type.BOSS);
this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER);
this.serverChannelInitializer = new ServerChannelInitializerHolder(new ServerChannelInitializer(this.server));
}
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 16,
1 << 18);
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
private final Set<Channel> endpoints = new HashSet<>();
private final TransportType transportType;
private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup;
private final VelocityServer server;
public final ServerChannelInitializerHolder serverChannelInitializer;
public void logChannelInformation() {
LOGGER.info("Connections will use {} channels, {} compression, {} ciphers", this.transportType, Natives.compressor.getLoadedVariant(), Natives.cipher.getLoadedVariant());
}
public ConnectionManager(VelocityServer server) {
this.server = server;
this.transportType = TransportType.bestType();
this.bossGroup = this.transportType.createEventLoopGroup(TransportType.Type.BOSS);
this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER);
this.serverChannelInitializer = new ServerChannelInitializerHolder(
new ServerChannelInitializer(this.server));
}
public void bind(final InetSocketAddress address) {
final ServerBootstrap bootstrap = new ServerBootstrap()
.channel(this.transportType.serverSocketChannelClass)
.group(this.bossGroup, this.workerGroup)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.childHandler(this.serverChannelInitializer.get())
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.IP_TOS, 0x18)
.localAddress(address);
bootstrap.bind()
.addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel();
if (future.isSuccess()) {
this.endpoints.add(channel);
LOGGER.info("Listening on {}", channel.localAddress());
} else {
LOGGER.error("Can't bind to {}", address, future.cause());
}
});
}
public void logChannelInformation() {
LOGGER.info("Connections will use {} channels, {} compression, {} ciphers", this.transportType,
Natives.compressor.getLoadedVariant(), Natives.cipher.getLoadedVariant());
}
public void queryBind(final String hostname, final int port) {
final Bootstrap bootstrap = new Bootstrap()
.channel(this.transportType.datagramChannelClass)
.group(this.workerGroup)
.handler(new GS4QueryHandler(this.server))
.localAddress(hostname, port);
bootstrap.bind()
.addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel();
if (future.isSuccess()) {
this.endpoints.add(channel);
LOGGER.info("Listening for GS4 query on {}", channel.localAddress());
} else {
LOGGER.error("Can't bind to {}", bootstrap.config().localAddress(), future.cause());
}
});
}
public void bind(final InetSocketAddress address) {
final ServerBootstrap bootstrap = new ServerBootstrap()
.channel(this.transportType.serverSocketChannelClass)
.group(this.bossGroup, this.workerGroup)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.childHandler(this.serverChannelInitializer.get())
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.IP_TOS, 0x18)
.localAddress(address);
bootstrap.bind()
.addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel();
if (future.isSuccess()) {
this.endpoints.add(channel);
LOGGER.info("Listening on {}", channel.localAddress());
} else {
LOGGER.error("Can't bind to {}", address, future.cause());
}
});
}
public Bootstrap createWorker() {
return new Bootstrap()
.channel(this.transportType.socketChannelClass)
.group(this.workerGroup)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.server.getConfiguration().getConnectTimeout());
}
public void queryBind(final String hostname, final int port) {
final Bootstrap bootstrap = new Bootstrap()
.channel(this.transportType.datagramChannelClass)
.group(this.workerGroup)
.handler(new GS4QueryHandler(this.server))
.localAddress(hostname, port);
bootstrap.bind()
.addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel();
if (future.isSuccess()) {
this.endpoints.add(channel);
LOGGER.info("Listening for GS4 query on {}", channel.localAddress());
} else {
LOGGER.error("Can't bind to {}", bootstrap.config().localAddress(), future.cause());
}
});
}
public void shutdown() {
for (final Channel endpoint : this.endpoints) {
try {
LOGGER.info("Closing endpoint {}", endpoint.localAddress());
endpoint.close().sync();
} catch (final InterruptedException e) {
LOGGER.info("Interrupted whilst closing endpoint", e);
Thread.currentThread().interrupt();
}
}
}
public Bootstrap createWorker() {
return new Bootstrap()
.channel(this.transportType.socketChannelClass)
.group(this.workerGroup)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
this.server.getConfiguration().getConnectTimeout());
}
public ServerChannelInitializerHolder getServerChannelInitializer() {
return this.serverChannelInitializer;
public void shutdown() {
for (final Channel endpoint : this.endpoints) {
try {
LOGGER.info("Closing endpoint {}", endpoint.localAddress());
endpoint.close().sync();
} catch (final InterruptedException e) {
LOGGER.info("Interrupted whilst closing endpoint", e);
Thread.currentThread().interrupt();
}
}
}
public ServerChannelInitializerHolder getServerChannelInitializer() {
return this.serverChannelInitializer;
}
}

View File

@@ -1,20 +1,21 @@
package com.velocitypowered.proxy.network;
public class Connections {
public static final String CIPHER_DECODER = "cipher-decoder";
public static final String CIPHER_ENCODER = "cipher-encoder";
public static final String COMPRESSION_DECODER = "compression-decoder";
public static final String COMPRESSION_ENCODER = "compression-encoder";
public static final String FRAME_DECODER = "frame-decoder";
public static final String FRAME_ENCODER = "frame-encoder";
public static final String HANDLER = "handler";
public static final String LEGACY_PING_DECODER = "legacy-ping-decoder";
public static final String LEGACY_PING_ENCODER = "legacy-ping-encoder";
public static final String MINECRAFT_DECODER = "minecraft-decoder";
public static final String MINECRAFT_ENCODER = "minecraft-encoder";
public static final String READ_TIMEOUT = "read-timeout";
private Connections() {
throw new AssertionError();
}
public static final String CIPHER_DECODER = "cipher-decoder";
public static final String CIPHER_ENCODER = "cipher-encoder";
public static final String COMPRESSION_DECODER = "compression-decoder";
public static final String COMPRESSION_ENCODER = "compression-encoder";
public static final String FRAME_DECODER = "frame-decoder";
public static final String FRAME_ENCODER = "frame-encoder";
public static final String HANDLER = "handler";
public static final String LEGACY_PING_DECODER = "legacy-ping-decoder";
public static final String LEGACY_PING_ENCODER = "legacy-ping-encoder";
public static final String MINECRAFT_DECODER = "minecraft-decoder";
public static final String MINECRAFT_ENCODER = "minecraft-encoder";
public static final String READ_TIMEOUT = "read-timeout";
private Connections() {
throw new AssertionError();
}
}

View File

@@ -1,5 +1,13 @@
package com.velocitypowered.proxy.network;
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.LEGACY_PING_DECODER;
import static com.velocitypowered.proxy.network.Connections.LEGACY_PING_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.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
@@ -15,43 +23,37 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.util.concurrent.TimeUnit;
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.LEGACY_PING_DECODER;
import static com.velocitypowered.proxy.network.Connections.LEGACY_PING_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;
@SuppressWarnings("WeakerAccess")
public class ServerChannelInitializer extends ChannelInitializer<Channel> {
private final VelocityServer server;
public ServerChannelInitializer(final VelocityServer server) {
this.server = server;
}
@Override
protected void initChannel(final Channel ch) {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(this.server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS))
.addLast(LEGACY_PING_DECODER, new LegacyPingDecoder())
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE)
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.SERVERBOUND))
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.CLIENTBOUND));
final MinecraftConnection connection = new MinecraftConnection(ch, this.server);
connection.setState(StateRegistry.HANDSHAKE);
connection.setSessionHandler(new HandshakeSessionHandler(connection, this.server));
ch.pipeline().addLast(Connections.HANDLER, connection);
if (ServerChannelInitializer.this.server.getConfiguration().isProxyProtocol()) {
ch.pipeline().addFirst(new HAProxyMessageDecoder());
}
private final VelocityServer server;
public ServerChannelInitializer(final VelocityServer server) {
this.server = server;
}
@Override
protected void initChannel(final Channel ch) {
ch.pipeline()
.addLast(READ_TIMEOUT,
new ReadTimeoutHandler(this.server.getConfiguration().getReadTimeout(),
TimeUnit.SECONDS))
.addLast(LEGACY_PING_DECODER, new LegacyPingDecoder())
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE)
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.SERVERBOUND))
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.CLIENTBOUND));
final MinecraftConnection connection = new MinecraftConnection(ch, this.server);
connection.setState(StateRegistry.HANDSHAKE);
connection.setSessionHandler(new HandshakeSessionHandler(connection, this.server));
ch.pipeline().addLast(Connections.HANDLER, connection);
if (ServerChannelInitializer.this.server.getConfiguration().isProxyProtocol()) {
ch.pipeline().addFirst(new HAProxyMessageDecoder());
}
}
}

View File

@@ -2,27 +2,28 @@ 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;
import java.util.function.Supplier;
public class ServerChannelInitializerHolder implements Supplier<ChannelInitializer<Channel>> {
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
private ChannelInitializer<Channel> initializer;
public ServerChannelInitializerHolder(final ChannelInitializer<Channel> initializer) {
this.initializer = initializer;
}
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
private ChannelInitializer<Channel> initializer;
@Override
public ChannelInitializer<Channel> get() {
return this.initializer;
}
public ServerChannelInitializerHolder(final ChannelInitializer<Channel> initializer) {
this.initializer = initializer;
}
@Deprecated
public void set(final ChannelInitializer<Channel> initializer) {
LOGGER.warn("The server channel initializer has been replaced by {}", Thread.currentThread().getStackTrace()[2]);
this.initializer = initializer;
}
@Override
public ChannelInitializer<Channel> get() {
return this.initializer;
}
@Deprecated
public void set(final ChannelInitializer<Channel> initializer) {
LOGGER.warn("The server channel initializer has been replaced by {}",
Thread.currentThread().getStackTrace()[2]);
this.initializer = initializer;
}
}

View File

@@ -19,68 +19,76 @@ import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.util.concurrent.ThreadFactory;
import java.util.function.BiFunction;
enum TransportType {
NIO("NIO", NioServerSocketChannel.class, NioSocketChannel.class, NioDatagramChannel.class, (name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class, EpollDatagramChannel.class, (name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))),
KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class, KQueueDatagramChannel.class, (name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)));
NIO("NIO", NioServerSocketChannel.class, NioSocketChannel.class, NioDatagramChannel.class,
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class,
EpollDatagramChannel.class,
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))),
KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class,
KQueueDatagramChannel.class,
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)));
final String name;
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
final Class<? extends SocketChannel> socketChannelClass;
final Class<? extends DatagramChannel> datagramChannelClass;
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory;
final String name;
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
final Class<? extends SocketChannel> socketChannelClass;
final Class<? extends DatagramChannel> datagramChannelClass;
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory;
TransportType(final String name, final Class<? extends ServerSocketChannel> serverSocketChannelClass, final Class<? extends SocketChannel> socketChannelClass, final Class<? extends DatagramChannel> datagramChannelClass, final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory) {
this.name = name;
this.serverSocketChannelClass = serverSocketChannelClass;
this.socketChannelClass = socketChannelClass;
this.datagramChannelClass = datagramChannelClass;
this.eventLoopGroupFactory = eventLoopGroupFactory;
TransportType(final String name,
final Class<? extends ServerSocketChannel> serverSocketChannelClass,
final Class<? extends SocketChannel> socketChannelClass,
final Class<? extends DatagramChannel> datagramChannelClass,
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory) {
this.name = name;
this.serverSocketChannelClass = serverSocketChannelClass;
this.socketChannelClass = socketChannelClass;
this.datagramChannelClass = datagramChannelClass;
this.eventLoopGroupFactory = eventLoopGroupFactory;
}
@Override
public String toString() {
return this.name;
}
public EventLoopGroup createEventLoopGroup(final Type type) {
return this.eventLoopGroupFactory.apply(this.name, type);
}
private static ThreadFactory createThreadFactory(final String name, final Type type) {
return new ThreadFactoryBuilder()
.setNameFormat("Netty " + name + ' ' + type.toString() + " #%d")
.setDaemon(true)
.build();
}
public static TransportType bestType() {
if (Epoll.isAvailable()) {
return EPOLL;
} else if (KQueue.isAvailable()) {
return KQUEUE;
} else {
return NIO;
}
}
public enum Type {
BOSS("Boss"),
WORKER("Worker");
private final String name;
Type(final String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
public EventLoopGroup createEventLoopGroup(final Type type) {
return this.eventLoopGroupFactory.apply(this.name, type);
}
private static ThreadFactory createThreadFactory(final String name, final Type type) {
return new ThreadFactoryBuilder()
.setNameFormat("Netty " + name + ' ' + type.toString() + " #%d")
.setDaemon(true)
.build();
}
public static TransportType bestType() {
if (Epoll.isAvailable()) {
return EPOLL;
} else if (KQueue.isAvailable()) {
return KQUEUE;
} else {
return NIO;
}
}
public enum Type {
BOSS("Boss"),
WORKER("Worker");
private final String name;
Type(final String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
return this.name;
}
}
}

View File

@@ -4,82 +4,91 @@ import com.google.common.base.VerifyException;
import com.velocitypowered.proxy.VelocityServer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.pool.*;
import io.netty.handler.codec.http.*;
import io.netty.channel.pool.AbstractChannelPoolMap;
import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.channel.pool.ChannelPoolMap;
import io.netty.channel.pool.FixedChannelPool;
import io.netty.channel.pool.SimpleChannelPool;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import javax.net.ssl.SSLEngine;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import javax.net.ssl.SSLEngine;
public class NettyHttpClient {
private final ChannelPoolMap<InetSocketAddress, SimpleChannelPool> poolMap;
public NettyHttpClient(VelocityServer server) {
Bootstrap bootstrap = server.initializeGenericBootstrap();
this.poolMap = new AbstractChannelPoolMap<InetSocketAddress, SimpleChannelPool>() {
@Override
protected SimpleChannelPool newPool(InetSocketAddress key) {
return new FixedChannelPool(bootstrap.remoteAddress(key), new ChannelPoolHandler() {
@Override
public void channelReleased(Channel channel) throws Exception {
channel.pipeline().remove("collector");
}
private final ChannelPoolMap<InetSocketAddress, SimpleChannelPool> poolMap;
@Override
public void channelAcquired(Channel channel) throws Exception {
// We don't do anything special when acquiring channels. The channel handler cleans up after
// each connection is used.
}
public NettyHttpClient(VelocityServer server) {
Bootstrap bootstrap = server.initializeGenericBootstrap();
this.poolMap = new AbstractChannelPoolMap<InetSocketAddress, SimpleChannelPool>() {
@Override
protected SimpleChannelPool newPool(InetSocketAddress key) {
return new FixedChannelPool(bootstrap.remoteAddress(key), new ChannelPoolHandler() {
@Override
public void channelReleased(Channel channel) throws Exception {
channel.pipeline().remove("collector");
}
@Override
public void channelCreated(Channel channel) throws Exception {
if (key.getPort() == 443) {
SslContext context = SslContextBuilder.forClient().build();
SSLEngine engine = context.newEngine(channel.alloc());
channel.pipeline().addLast("ssl", new SslHandler(engine));
}
channel.pipeline().addLast("http", new HttpClientCodec());
}
}, 8);
@Override
public void channelAcquired(Channel channel) throws Exception {
// We don't do anything special when acquiring channels. The channel handler cleans up after
// each connection is used.
}
@Override
public void channelCreated(Channel channel) throws Exception {
if (key.getPort() == 443) {
SslContext context = SslContextBuilder.forClient().build();
SSLEngine engine = context.newEngine(channel.alloc());
channel.pipeline().addLast("ssl", new SslHandler(engine));
}
};
channel.pipeline().addLast("http", new HttpClientCodec());
}
}, 8);
}
};
}
public CompletableFuture<SimpleHttpResponse> get(URL url) {
String host = url.getHost();
int port = url.getPort();
boolean ssl = url.getProtocol().equals("https");
if (port == -1) {
port = ssl ? 443 : 80;
}
public CompletableFuture<SimpleHttpResponse> get(URL url) {
String host = url.getHost();
int port = url.getPort();
boolean ssl = url.getProtocol().equals("https");
if (port == -1) {
port = ssl ? 443 : 80;
}
CompletableFuture<SimpleHttpResponse> reply = new CompletableFuture<>();
InetSocketAddress address = new InetSocketAddress(host, port);
poolMap.get(address)
.acquire()
.addListener(future -> {
if (future.isSuccess()) {
Channel channel = (Channel) future.getNow();
if (channel == null) {
throw new VerifyException("Null channel retrieved from pool!");
}
channel.pipeline().addLast("collector", new SimpleHttpResponseCollector(reply));
CompletableFuture<SimpleHttpResponse> reply = new CompletableFuture<>();
InetSocketAddress address = new InetSocketAddress(host, port);
poolMap.get(address)
.acquire()
.addListener(future -> {
if (future.isSuccess()) {
Channel channel = (Channel) future.getNow();
if (channel == null) {
throw new VerifyException("Null channel retrieved from pool!");
}
channel.pipeline().addLast("collector", new SimpleHttpResponseCollector(reply));
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.GET, url.getPath() + "?" + url.getQuery());
request.headers().add(HttpHeaderNames.HOST, url.getHost());
request.headers().add(HttpHeaderNames.USER_AGENT, "Velocity");
channel.writeAndFlush(request);
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url.getPath() + "?" + url.getQuery());
request.headers().add(HttpHeaderNames.HOST, url.getHost());
request.headers().add(HttpHeaderNames.USER_AGENT, "Velocity");
channel.writeAndFlush(request);
// Make sure to release this connection
reply.whenComplete((resp, err) -> poolMap.get(address).release(channel));
} else {
reply.completeExceptionally(future.cause());
}
});
return reply;
}
// Make sure to release this connection
reply.whenComplete((resp, err) -> poolMap.get(address).release(channel));
} else {
reply.completeExceptionally(future.cause());
}
});
return reply;
}
}

View File

@@ -1,27 +1,28 @@
package com.velocitypowered.proxy.network.http;
public class SimpleHttpResponse {
private final int code;
private final String body;
SimpleHttpResponse(int code, String body) {
this.code = code;
this.body = body;
}
private final int code;
private final String body;
public int getCode() {
return code;
}
SimpleHttpResponse(int code, String body) {
this.code = code;
this.body = body;
}
public String getBody() {
return body;
}
public int getCode() {
return code;
}
@Override
public String toString() {
return "SimpleHttpResponse{" +
"code=" + code +
", body='" + body + '\'' +
'}';
}
public String getBody() {
return body;
}
@Override
public String toString() {
return "SimpleHttpResponse{" +
"code=" + code +
", body='" + body + '\'' +
'}';
}
}

View File

@@ -2,50 +2,54 @@ package com.velocitypowered.proxy.network.http;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.ReferenceCountUtil;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
class SimpleHttpResponseCollector extends ChannelInboundHandlerAdapter {
private final StringBuilder buffer = new StringBuilder(1024);
private final CompletableFuture<SimpleHttpResponse> reply;
private int httpCode;
private boolean canKeepAlive;
SimpleHttpResponseCollector(CompletableFuture<SimpleHttpResponse> reply) {
this.reply = reply;
}
private final StringBuilder buffer = new StringBuilder(1024);
private final CompletableFuture<SimpleHttpResponse> reply;
private int httpCode;
private boolean canKeepAlive;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
HttpResponseStatus status = response.status();
this.httpCode = status.code();
this.canKeepAlive = HttpUtil.isKeepAlive(response);
}
SimpleHttpResponseCollector(CompletableFuture<SimpleHttpResponse> reply) {
this.reply = reply;
}
if (msg instanceof HttpContent) {
buffer.append(((HttpContent) msg).content().toString(StandardCharsets.UTF_8));
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
HttpResponseStatus status = response.status();
this.httpCode = status.code();
this.canKeepAlive = HttpUtil.isKeepAlive(response);
}
if (msg instanceof LastHttpContent) {
if (!canKeepAlive) {
ctx.close();
}
reply.complete(new SimpleHttpResponse(httpCode, buffer.toString()));
}
}
} finally {
ReferenceCountUtil.release(msg);
if (msg instanceof HttpContent) {
buffer.append(((HttpContent) msg).content().toString(StandardCharsets.UTF_8));
if (msg instanceof LastHttpContent) {
if (!canKeepAlive) {
ctx.close();
}
reply.complete(new SimpleHttpResponse(httpCode, buffer.toString()));
}
}
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
reply.completeExceptionally(cause);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
reply.completeExceptionally(cause);
}
}

View File

@@ -8,52 +8,54 @@ import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class PluginClassLoader extends URLClassLoader {
private static final Set<PluginClassLoader> loaders = new CopyOnWriteArraySet<>();
static {
ClassLoader.registerAsParallelCapable();
private static final Set<PluginClassLoader> loaders = new CopyOnWriteArraySet<>();
static {
ClassLoader.registerAsParallelCapable();
}
public PluginClassLoader(URL[] urls) {
super(urls);
}
public void addToClassloaders() {
loaders.add(this);
}
void addPath(Path path) {
try {
addURL(path.toUri().toURL());
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return loadClass0(name, resolve, true);
}
private Class<?> loadClass0(String name, boolean resolve, boolean checkOther)
throws ClassNotFoundException {
try {
return super.loadClass(name, resolve);
} catch (ClassNotFoundException ignored) {
// Ignored: we'll try others
}
public PluginClassLoader(URL[] urls) {
super(urls);
}
public void addToClassloaders() {
loaders.add(this);
}
void addPath(Path path) {
try {
addURL(path.toUri().toURL());
} catch (MalformedURLException e) {
throw new AssertionError(e);
if (checkOther) {
for (PluginClassLoader loader : loaders) {
if (loader != this) {
try {
return loader.loadClass0(name, resolve, false);
} catch (ClassNotFoundException ignored) {
// We're trying others, safe to ignore
}
}
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return loadClass0(name, resolve, true);
}
private Class<?> loadClass0(String name, boolean resolve, boolean checkOther) throws ClassNotFoundException {
try {
return super.loadClass(name, resolve);
} catch (ClassNotFoundException ignored) {
// Ignored: we'll try others
}
if (checkOther) {
for (PluginClassLoader loader : loaders) {
if (loader != this) {
try {
return loader.loadClass0(name, resolve, false);
} catch (ClassNotFoundException ignored) {
// We're trying others, safe to ignore
}
}
}
}
throw new ClassNotFoundException(name);
}
throw new ClassNotFoundException(name);
}
}

View File

@@ -9,6 +9,16 @@ import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.plugin.PluginManager;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import net.kyori.event.EventSubscriber;
import net.kyori.event.PostResult;
import net.kyori.event.SimpleEventBus;
@@ -21,180 +31,183 @@ import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class VelocityEventManager implements EventManager {
private static final Logger logger = LogManager.getLogger(VelocityEventManager.class);
private final ListMultimap<Object, Object> registeredListenersByPlugin = Multimaps
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
private final ListMultimap<Object, EventHandler<?>> registeredHandlersByPlugin = Multimaps
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
private final SimpleEventBus<Object> bus;
private final MethodSubscriptionAdapter<Object> methodAdapter;
private final ExecutorService service;
private final PluginManager pluginManager;
private static final Logger logger = LogManager.getLogger(VelocityEventManager.class);
public VelocityEventManager(PluginManager pluginManager) {
PluginClassLoader cl = new PluginClassLoader(new URL[0]);
cl.addToClassloaders();
this.bus = new SimpleEventBus<>(Object.class);
this.methodAdapter = new SimpleMethodSubscriptionAdapter<>(bus, new ASMEventExecutorFactory<>(cl),
new VelocityMethodScanner());
this.pluginManager = pluginManager;
this.service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactoryBuilder()
.setNameFormat("Velocity Event Executor - #%d").setDaemon(true).build());
private final ListMultimap<Object, Object> registeredListenersByPlugin = Multimaps
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
private final ListMultimap<Object, EventHandler<?>> registeredHandlersByPlugin = Multimaps
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
private final SimpleEventBus<Object> bus;
private final MethodSubscriptionAdapter<Object> methodAdapter;
private final ExecutorService service;
private final PluginManager pluginManager;
public VelocityEventManager(PluginManager pluginManager) {
PluginClassLoader cl = new PluginClassLoader(new URL[0]);
cl.addToClassloaders();
this.bus = new SimpleEventBus<>(Object.class);
this.methodAdapter = new SimpleMethodSubscriptionAdapter<>(bus,
new ASMEventExecutorFactory<>(cl),
new VelocityMethodScanner());
this.pluginManager = pluginManager;
this.service = Executors
.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactoryBuilder()
.setNameFormat("Velocity Event Executor - #%d").setDaemon(true).build());
}
private void ensurePlugin(Object plugin) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(),
"Specified plugin is not loaded");
}
@Override
public void register(Object plugin, Object listener) {
ensurePlugin(plugin);
Preconditions.checkNotNull(listener, "listener");
if (plugin == listener && registeredListenersByPlugin.containsEntry(plugin, plugin)) {
throw new IllegalArgumentException(
"Trying to register the plugin main instance. Velocity already takes care of this for you.");
}
registeredListenersByPlugin.put(plugin, listener);
methodAdapter.register(listener);
}
@Override
@SuppressWarnings("type.argument.type.incompatible")
public <E> void register(Object plugin, Class<E> eventClass, PostOrder postOrder,
EventHandler<E> handler) {
ensurePlugin(plugin);
Preconditions.checkNotNull(eventClass, "eventClass");
Preconditions.checkNotNull(postOrder, "postOrder");
Preconditions.checkNotNull(handler, "listener");
bus.register(eventClass, new KyoriToVelocityHandler<>(handler, postOrder));
}
@Override
public <E> CompletableFuture<E> fire(E event) {
if (event == null) {
throw new NullPointerException("event");
}
if (!bus.hasSubscribers(event.getClass())) {
// Optimization: nobody's listening.
return CompletableFuture.completedFuture(event);
}
private void ensurePlugin(Object plugin) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
Runnable runEvent = () -> {
PostResult result = bus.post(event);
if (!result.exceptions().isEmpty()) {
logger.error("Some errors occurred whilst posting event {}.", event);
int i = 0;
for (Throwable exception : result.exceptions().values()) {
logger.error("#{}: \n", ++i, exception);
}
}
};
CompletableFuture<E> eventFuture = new CompletableFuture<>();
service.execute(() -> {
runEvent.run();
eventFuture.complete(event);
});
return eventFuture;
}
@Override
public void unregisterListeners(Object plugin) {
ensurePlugin(plugin);
Collection<Object> listeners = registeredListenersByPlugin.removeAll(plugin);
listeners.forEach(methodAdapter::unregister);
Collection<EventHandler<?>> handlers = registeredHandlersByPlugin.removeAll(plugin);
handlers
.forEach(handler -> bus.unregister(new KyoriToVelocityHandler<>(handler, PostOrder.LAST)));
}
@Override
public void unregisterListener(Object plugin, Object listener) {
ensurePlugin(plugin);
Preconditions.checkNotNull(listener, "listener");
registeredListenersByPlugin.remove(plugin, listener);
methodAdapter.unregister(listener);
}
@Override
public <E> void unregister(Object plugin, EventHandler<E> handler) {
ensurePlugin(plugin);
Preconditions.checkNotNull(handler, "listener");
registeredHandlersByPlugin.remove(plugin, handler);
bus.unregister(new KyoriToVelocityHandler<>(handler, PostOrder.LAST));
}
public boolean shutdown() throws InterruptedException {
service.shutdown();
return service.awaitTermination(10, TimeUnit.SECONDS);
}
private static class VelocityMethodScanner implements MethodScanner<Object> {
@Override
public boolean shouldRegister(@NonNull Object listener, @NonNull Method method) {
return method.isAnnotationPresent(Subscribe.class);
}
@Override
public void register(Object plugin, Object listener) {
ensurePlugin(plugin);
Preconditions.checkNotNull(listener, "listener");
if (plugin == listener && registeredListenersByPlugin.containsEntry(plugin, plugin)) {
throw new IllegalArgumentException("Trying to register the plugin main instance. Velocity already takes care of this for you.");
}
registeredListenersByPlugin.put(plugin, listener);
methodAdapter.register(listener);
public int postOrder(@NonNull Object listener, @NonNull Method method) {
Subscribe annotation = method.getAnnotation(Subscribe.class);
if (annotation == null) {
throw new IllegalStateException(
"Trying to determine post order for listener without @Subscribe annotation");
}
return annotation.order().ordinal();
}
@Override
@SuppressWarnings("type.argument.type.incompatible")
public <E> void register(Object plugin, Class<E> eventClass, PostOrder postOrder, EventHandler<E> handler) {
ensurePlugin(plugin);
Preconditions.checkNotNull(eventClass, "eventClass");
Preconditions.checkNotNull(postOrder, "postOrder");
Preconditions.checkNotNull(handler, "listener");
bus.register(eventClass, new KyoriToVelocityHandler<>(handler, postOrder));
public boolean consumeCancelledEvents(@NonNull Object listener, @NonNull Method method) {
return true;
}
}
private static class KyoriToVelocityHandler<E> implements EventSubscriber<E> {
private final EventHandler<E> handler;
private final int postOrder;
private KyoriToVelocityHandler(EventHandler<E> handler, PostOrder postOrder) {
this.handler = handler;
this.postOrder = postOrder.ordinal();
}
@Override
public <E> CompletableFuture<E> fire(E event) {
if (event == null) {
throw new NullPointerException("event");
}
if (!bus.hasSubscribers(event.getClass())) {
// Optimization: nobody's listening.
return CompletableFuture.completedFuture(event);
}
Runnable runEvent = () -> {
PostResult result = bus.post(event);
if (!result.exceptions().isEmpty()) {
logger.error("Some errors occurred whilst posting event {}.", event);
int i = 0;
for (Throwable exception : result.exceptions().values()) {
logger.error("#{}: \n", ++i, exception);
}
}
};
CompletableFuture<E> eventFuture = new CompletableFuture<>();
service.execute(() -> {
runEvent.run();
eventFuture.complete(event);
});
return eventFuture;
public void invoke(@NonNull E event) {
handler.execute(event);
}
@Override
public void unregisterListeners(Object plugin) {
ensurePlugin(plugin);
Collection<Object> listeners = registeredListenersByPlugin.removeAll(plugin);
listeners.forEach(methodAdapter::unregister);
Collection<EventHandler<?>> handlers = registeredHandlersByPlugin.removeAll(plugin);
handlers.forEach(handler -> bus.unregister(new KyoriToVelocityHandler<>(handler, PostOrder.LAST)));
public int postOrder() {
return postOrder;
}
public EventHandler<E> getHandler() {
return handler;
}
@Override
public void unregisterListener(Object plugin, Object listener) {
ensurePlugin(plugin);
Preconditions.checkNotNull(listener, "listener");
registeredListenersByPlugin.remove(plugin, listener);
methodAdapter.unregister(listener);
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
KyoriToVelocityHandler<?> that = (KyoriToVelocityHandler<?>) o;
return Objects.equals(handler, that.handler);
}
@Override
public <E> void unregister(Object plugin, EventHandler<E> handler) {
ensurePlugin(plugin);
Preconditions.checkNotNull(handler, "listener");
registeredHandlersByPlugin.remove(plugin, handler);
bus.unregister(new KyoriToVelocityHandler<>(handler, PostOrder.LAST));
}
public boolean shutdown() throws InterruptedException {
service.shutdown();
return service.awaitTermination(10, TimeUnit.SECONDS);
}
private static class VelocityMethodScanner implements MethodScanner<Object> {
@Override
public boolean shouldRegister(@NonNull Object listener, @NonNull Method method) {
return method.isAnnotationPresent(Subscribe.class);
}
@Override
public int postOrder(@NonNull Object listener, @NonNull Method method) {
Subscribe annotation = method.getAnnotation(Subscribe.class);
if (annotation == null) {
throw new IllegalStateException("Trying to determine post order for listener without @Subscribe annotation");
}
return annotation.order().ordinal();
}
@Override
public boolean consumeCancelledEvents(@NonNull Object listener, @NonNull Method method) {
return true;
}
}
private static class KyoriToVelocityHandler<E> implements EventSubscriber<E> {
private final EventHandler<E> handler;
private final int postOrder;
private KyoriToVelocityHandler(EventHandler<E> handler, PostOrder postOrder) {
this.handler = handler;
this.postOrder = postOrder.ordinal();
}
@Override
public void invoke(@NonNull E event) {
handler.execute(event);
}
@Override
public int postOrder() {
return postOrder;
}
public EventHandler<E> getHandler() {
return handler;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KyoriToVelocityHandler<?> that = (KyoriToVelocityHandler<?>) o;
return Objects.equals(handler, that.handler);
}
@Override
public int hashCode() {
return Objects.hash(handler);
}
public int hashCode() {
return Objects.hash(handler);
}
}
}

View File

@@ -1,5 +1,8 @@
package com.velocitypowered.proxy.plugin;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginManager;
@@ -7,124 +10,131 @@ import com.velocitypowered.api.plugin.meta.PluginDependency;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.plugin.loader.JavaPluginLoader;
import com.velocitypowered.proxy.plugin.util.PluginDependencyUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class VelocityPluginManager implements PluginManager {
private static final Logger logger = LogManager.getLogger(VelocityPluginManager.class);
private final Map<String, PluginContainer> plugins = new HashMap<>();
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
private final VelocityServer server;
private static final Logger logger = LogManager.getLogger(VelocityPluginManager.class);
public VelocityPluginManager(VelocityServer server) {
this.server = checkNotNull(server, "server");
private final Map<String, PluginContainer> plugins = new HashMap<>();
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
private final VelocityServer server;
public VelocityPluginManager(VelocityServer server) {
this.server = checkNotNull(server, "server");
}
private void registerPlugin(PluginContainer plugin) {
plugins.put(plugin.getDescription().getId(), plugin);
Optional<?> instance = plugin.getInstance();
if (instance.isPresent()) {
pluginInstances.put(instance.get(), plugin);
}
}
private void registerPlugin(PluginContainer plugin) {
plugins.put(plugin.getDescription().getId(), plugin);
Optional<?> instance = plugin.getInstance();
if (instance.isPresent()) {
pluginInstances.put(instance.get(), plugin);
public void loadPlugins(Path directory) throws IOException {
checkNotNull(directory, "directory");
checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory");
List<PluginDescription> found = new ArrayList<>();
JavaPluginLoader loader = new JavaPluginLoader(server, directory);
try (DirectoryStream<Path> stream = Files
.newDirectoryStream(directory, p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) {
for (Path path : stream) {
try {
found.add(loader.loadPlugin(path));
} catch (Exception e) {
logger.error("Unable to load plugin {}", path, e);
}
}
}
public void loadPlugins(Path directory) throws IOException {
checkNotNull(directory, "directory");
checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory");
if (found.isEmpty()) {
// No plugins found
return;
}
List<PluginDescription> found = new ArrayList<>();
JavaPluginLoader loader = new JavaPluginLoader(server, directory);
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) {
for (Path path : stream) {
try {
found.add(loader.loadPlugin(path));
} catch (Exception e) {
logger.error("Unable to load plugin {}", path, e);
}
}
// Now load the plugins
pluginLoad:
for (PluginDescription plugin : sortedPlugins) {
// Verify dependencies
for (PluginDependency dependency : plugin.getDependencies()) {
if (!dependency.isOptional() && !isLoaded(dependency.getId())) {
logger.error("Can't load plugin {} due to missing dependency {}", plugin.getId(),
dependency.getId());
continue pluginLoad;
}
}
if (found.isEmpty()) {
// No plugins found
return;
}
// Actually create the plugin
PluginContainer pluginObject;
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found);
try {
pluginObject = loader.createPlugin(plugin);
} catch (Exception e) {
logger.error("Can't create plugin {}", plugin.getId(), e);
continue;
}
// Now load the plugins
pluginLoad:
for (PluginDescription plugin : sortedPlugins) {
// Verify dependencies
for (PluginDependency dependency : plugin.getDependencies()) {
if (!dependency.isOptional() && !isLoaded(dependency.getId())) {
logger.error("Can't load plugin {} due to missing dependency {}", plugin.getId(), dependency.getId());
continue pluginLoad;
}
}
registerPlugin(pluginObject);
}
}
// Actually create the plugin
PluginContainer pluginObject;
@Override
public Optional<PluginContainer> fromInstance(Object instance) {
checkNotNull(instance, "instance");
try {
pluginObject = loader.createPlugin(plugin);
} catch (Exception e) {
logger.error("Can't create plugin {}", plugin.getId(), e);
continue;
}
registerPlugin(pluginObject);
}
if (instance instanceof PluginContainer) {
return Optional.of((PluginContainer) instance);
}
@Override
public Optional<PluginContainer> fromInstance(Object instance) {
checkNotNull(instance, "instance");
return Optional.ofNullable(pluginInstances.get(instance));
}
if (instance instanceof PluginContainer) {
return Optional.of((PluginContainer) instance);
}
@Override
public Optional<PluginContainer> getPlugin(String id) {
checkNotNull(id, "id");
return Optional.ofNullable(plugins.get(id));
}
return Optional.ofNullable(pluginInstances.get(instance));
}
@Override
public Optional<PluginContainer> getPlugin(String id) {
checkNotNull(id, "id");
return Optional.ofNullable(plugins.get(id));
}
@Override
public Collection<PluginContainer> getPlugins() {
return Collections.unmodifiableCollection(plugins.values());
}
@Override
public boolean isLoaded(String id) {
return plugins.containsKey(id);
}
@Override
public void addToClasspath(Object plugin, Path path) {
checkNotNull(plugin, "instance");
checkNotNull(path, "path");
checkArgument(pluginInstances.containsKey(plugin), "plugin is not loaded");
ClassLoader pluginClassloader = plugin.getClass().getClassLoader();
if (pluginClassloader instanceof PluginClassLoader) {
((PluginClassLoader) pluginClassloader).addPath(path);
} else {
throw new UnsupportedOperationException("Operation is not supported on non-Java Velocity plugins.");
}
@Override
public Collection<PluginContainer> getPlugins() {
return Collections.unmodifiableCollection(plugins.values());
}
@Override
public boolean isLoaded(String id) {
return plugins.containsKey(id);
}
@Override
public void addToClasspath(Object plugin, Path path) {
checkNotNull(plugin, "instance");
checkNotNull(path, "path");
checkArgument(pluginInstances.containsKey(plugin), "plugin is not loaded");
ClassLoader pluginClassloader = plugin.getClass().getClassLoader();
if (pluginClassloader instanceof PluginClassLoader) {
((PluginClassLoader) pluginClassloader).addPath(path);
} else {
throw new UnsupportedOperationException(
"Operation is not supported on non-Java Velocity plugins.");
}
}
}

View File

@@ -12,7 +12,6 @@ import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.plugin.PluginClassLoader;
import com.velocitypowered.proxy.plugin.loader.java.JavaVelocityPluginDescription;
import com.velocitypowered.proxy.plugin.loader.java.VelocityPluginModule;
import java.io.BufferedInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
@@ -26,100 +25,108 @@ import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
public class JavaPluginLoader implements PluginLoader {
private final ProxyServer server;
private final Path baseDirectory;
public JavaPluginLoader(ProxyServer server, Path baseDirectory) {
this.server = server;
this.baseDirectory = baseDirectory;
private final ProxyServer server;
private final Path baseDirectory;
public JavaPluginLoader(ProxyServer server, Path baseDirectory) {
this.server = server;
this.baseDirectory = baseDirectory;
}
@Override
public PluginDescription loadPlugin(Path source) throws Exception {
Optional<SerializedPluginDescription> serialized = getSerializedPluginInfo(source);
if (!serialized.isPresent()) {
throw new InvalidPluginException("Did not find a valid velocity-info.json.");
}
@Override
public PluginDescription loadPlugin(Path source) throws Exception {
Optional<SerializedPluginDescription> serialized = getSerializedPluginInfo(source);
if (!serialized.isPresent()) {
throw new InvalidPluginException("Did not find a valid velocity-info.json.");
}
SerializedPluginDescription pd = serialized.get();
if (!PluginDescription.ID_PATTERN.matcher(pd.getId()).matches()) {
throw new InvalidPluginException("Plugin ID '" + pd.getId() + "' must match pattern " +
PluginDescription.ID_PATTERN.pattern());
}
PluginClassLoader loader = new PluginClassLoader(
new URL[] {source.toUri().toURL() }
);
loader.addToClassloaders();
Class mainClass = loader.loadClass(pd.getMain());
return createDescription(pd, source, mainClass);
SerializedPluginDescription pd = serialized.get();
if (!PluginDescription.ID_PATTERN.matcher(pd.getId()).matches()) {
throw new InvalidPluginException("Plugin ID '" + pd.getId() + "' must match pattern " +
PluginDescription.ID_PATTERN.pattern());
}
@Override
public PluginContainer createPlugin(PluginDescription description) throws Exception {
if (!(description instanceof JavaVelocityPluginDescription)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
}
PluginClassLoader loader = new PluginClassLoader(
new URL[]{source.toUri().toURL()}
);
loader.addToClassloaders();
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
Optional<Path> source = javaDescription.getSource();
Class mainClass = loader.loadClass(pd.getMain());
return createDescription(pd, source, mainClass);
}
if (!source.isPresent()) {
throw new IllegalArgumentException("No path in plugin description");
}
Injector injector = Guice.createInjector(new VelocityPluginModule(server, javaDescription, baseDirectory));
Object instance = injector.getInstance(javaDescription.getMainClass());
if (instance == null) {
throw new IllegalStateException("Got nothing from injector for plugin " + javaDescription.getId());
}
return new VelocityPluginContainer(description, instance);
@Override
public PluginContainer createPlugin(PluginDescription description) throws Exception {
if (!(description instanceof JavaVelocityPluginDescription)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
}
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source) throws Exception {
try (JarInputStream in = new JarInputStream(new BufferedInputStream(Files.newInputStream(source)))) {
JarEntry entry;
while ((entry = in.getNextJarEntry()) != null) {
if (entry.getName().equals("velocity-plugin.json")) {
try (Reader pluginInfoReader = new InputStreamReader(in)) {
return Optional.of(VelocityServer.GSON.fromJson(pluginInfoReader, SerializedPluginDescription.class));
}
}
}
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
Optional<Path> source = javaDescription.getSource();
return Optional.empty();
if (!source.isPresent()) {
throw new IllegalArgumentException("No path in plugin description");
}
Injector injector = Guice
.createInjector(new VelocityPluginModule(server, javaDescription, baseDirectory));
Object instance = injector.getInstance(javaDescription.getMainClass());
if (instance == null) {
throw new IllegalStateException(
"Got nothing from injector for plugin " + javaDescription.getId());
}
return new VelocityPluginContainer(description, instance);
}
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source)
throws Exception {
try (JarInputStream in = new JarInputStream(
new BufferedInputStream(Files.newInputStream(source)))) {
JarEntry entry;
while ((entry = in.getNextJarEntry()) != null) {
if (entry.getName().equals("velocity-plugin.json")) {
try (Reader pluginInfoReader = new InputStreamReader(in)) {
return Optional.of(VelocityServer.GSON
.fromJson(pluginInfoReader, SerializedPluginDescription.class));
}
}
}
return Optional.empty();
}
}
private VelocityPluginDescription createDescription(SerializedPluginDescription description,
Path source, Class mainClass) {
Set<PluginDependency> dependencies = new HashSet<>();
for (SerializedPluginDescription.Dependency dependency : description.getDependencies()) {
dependencies.add(toDependencyMeta(dependency));
}
private VelocityPluginDescription createDescription(SerializedPluginDescription description, Path source, Class mainClass) {
Set<PluginDependency> dependencies = new HashSet<>();
return new JavaVelocityPluginDescription(
description.getId(),
description.getName(),
description.getVersion(),
description.getDescription(),
description.getUrl(),
description.getAuthors(),
dependencies,
source,
mainClass
);
}
for (SerializedPluginDescription.Dependency dependency : description.getDependencies()) {
dependencies.add(toDependencyMeta(dependency));
}
return new JavaVelocityPluginDescription(
description.getId(),
description.getName(),
description.getVersion(),
description.getDescription(),
description.getUrl(),
description.getAuthors(),
dependencies,
source,
mainClass
);
}
private static PluginDependency toDependencyMeta(SerializedPluginDescription.Dependency dependency) {
return new PluginDependency(
dependency.getId(),
null, // TODO Implement version matching in dependency annotation
dependency.isOptional()
);
}
private static PluginDependency toDependencyMeta(
SerializedPluginDescription.Dependency dependency) {
return new PluginDependency(
dependency.getId(),
null, // TODO Implement version matching in dependency annotation
dependency.isOptional()
);
}
}

View File

@@ -2,14 +2,14 @@ package com.velocitypowered.proxy.plugin.loader;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import java.nio.file.Path;
/**
* This interface is used for loading plugins.
*/
public interface PluginLoader {
PluginDescription loadPlugin(Path source) throws Exception;
PluginContainer createPlugin(PluginDescription plugin) throws Exception;
PluginDescription loadPlugin(Path source) throws Exception;
PluginContainer createPlugin(PluginDescription plugin) throws Exception;
}

View File

@@ -2,25 +2,25 @@ package com.velocitypowered.proxy.plugin.loader;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import java.util.Optional;
public class VelocityPluginContainer implements PluginContainer {
private final PluginDescription description;
private final Object instance;
public VelocityPluginContainer(PluginDescription description, Object instance) {
this.description = description;
this.instance = instance;
}
private final PluginDescription description;
private final Object instance;
@Override
public PluginDescription getDescription() {
return this.description;
}
public VelocityPluginContainer(PluginDescription description, Object instance) {
this.description = description;
this.instance = instance;
}
@Override
public Optional<?> getInstance() {
return Optional.ofNullable(instance);
}
@Override
public PluginDescription getDescription() {
return this.description;
}
@Override
public Optional<?> getInstance() {
return Optional.ofNullable(instance);
}
}

View File

@@ -1,98 +1,99 @@
package com.velocitypowered.proxy.plugin.loader;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkNotNull;
import org.checkerframework.checker.nullness.qual.Nullable;
public class VelocityPluginDescription implements PluginDescription {
private final String id;
private final @Nullable String name;
private final @Nullable String version;
private final @Nullable String description;
private final @Nullable String url;
private final List<String> authors;
private final Map<String, PluginDependency> dependencies;
private final Path source;
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source) {
this.id = checkNotNull(id, "id");
this.name = Strings.emptyToNull(name);
this.version = Strings.emptyToNull(version);
this.description = Strings.emptyToNull(description);
this.url = Strings.emptyToNull(url);
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
this.source = source;
}
private final String id;
private final @Nullable String name;
private final @Nullable String version;
private final @Nullable String description;
private final @Nullable String url;
private final List<String> authors;
private final Map<String, PluginDependency> dependencies;
private final Path source;
@Override
public String getId() {
return id;
}
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source) {
this.id = checkNotNull(id, "id");
this.name = Strings.emptyToNull(name);
this.version = Strings.emptyToNull(version);
this.description = Strings.emptyToNull(description);
this.url = Strings.emptyToNull(url);
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
this.source = source;
}
@Override
public Optional<String> getName() {
return Optional.ofNullable(name);
}
@Override
public String getId() {
return id;
}
@Override
public Optional<String> getVersion() {
return Optional.ofNullable(version);
}
@Override
public Optional<String> getName() {
return Optional.ofNullable(name);
}
@Override
public Optional<String> getDescription() {
return Optional.ofNullable(description);
}
@Override
public Optional<String> getVersion() {
return Optional.ofNullable(version);
}
@Override
public Optional<String> getUrl() {
return Optional.ofNullable(url);
}
@Override
public Optional<String> getDescription() {
return Optional.ofNullable(description);
}
@Override
public List<String> getAuthors() {
return authors;
}
@Override
public Optional<String> getUrl() {
return Optional.ofNullable(url);
}
@Override
public Collection<PluginDependency> getDependencies() {
return dependencies.values();
}
@Override
public List<String> getAuthors() {
return authors;
}
@Override
public Optional<PluginDependency> getDependency(String id) {
return Optional.ofNullable(dependencies.get(id));
}
@Override
public Collection<PluginDependency> getDependencies() {
return dependencies.values();
}
@Override
public Optional<Path> getSource() {
return Optional.ofNullable(source);
}
@Override
public Optional<PluginDependency> getDependency(String id) {
return Optional.ofNullable(dependencies.get(id));
}
@Override
public String toString() {
return "VelocityPluginDescription{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", version='" + version + '\'' +
", description='" + description + '\'' +
", url='" + url + '\'' +
", authors=" + authors +
", dependencies=" + dependencies +
", source=" + source +
'}';
}
@Override
public Optional<Path> getSource() {
return Optional.ofNullable(source);
}
@Override
public String toString() {
return "VelocityPluginDescription{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", version='" + version + '\'' +
", description='" + description + '\'' +
", url='" + url + '\'' +
", authors=" + authors +
", dependencies=" + dependencies +
", source=" + source +
'}';
}
}

View File

@@ -1,25 +1,27 @@
package com.velocitypowered.proxy.plugin.loader.java;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;
public class JavaVelocityPluginDescription extends VelocityPluginDescription {
private final Class<?> mainClass;
public JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source, Class<?> mainClass) {
super(id, name, version, description, url, authors, dependencies, source);
this.mainClass = checkNotNull(mainClass);
}
private final Class<?> mainClass;
public Class<?> getMainClass() {
return mainClass;
}
public JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source,
Class<?> mainClass) {
super(id, name, version, description, url, authors, dependencies, source);
this.mainClass = checkNotNull(mainClass);
}
public Class<?> getMainClass() {
return mainClass;
}
}

View File

@@ -8,30 +8,32 @@ import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import java.nio.file.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Path;
public class VelocityPluginModule implements Module {
private final ProxyServer server;
private final JavaVelocityPluginDescription description;
private final Path basePluginPath;
public VelocityPluginModule(ProxyServer server, JavaVelocityPluginDescription description, Path basePluginPath) {
this.server = server;
this.description = description;
this.basePluginPath = basePluginPath;
}
private final ProxyServer server;
private final JavaVelocityPluginDescription description;
private final Path basePluginPath;
@Override
public void configure(Binder binder) {
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId()));
binder.bind(ProxyServer.class).toInstance(server);
binder.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(basePluginPath.resolve(description.getId()));
binder.bind(PluginDescription.class).toInstance(description);
binder.bind(PluginManager.class).toInstance(server.getPluginManager());
binder.bind(EventManager.class).toInstance(server.getEventManager());
binder.bind(CommandManager.class).toInstance(server.getCommandManager());
}
public VelocityPluginModule(ProxyServer server, JavaVelocityPluginDescription description,
Path basePluginPath) {
this.server = server;
this.description = description;
this.basePluginPath = basePluginPath;
}
@Override
public void configure(Binder binder) {
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId()));
binder.bind(ProxyServer.class).toInstance(server);
binder.bind(Path.class).annotatedWith(DataDirectory.class)
.toInstance(basePluginPath.resolve(description.getId()));
binder.bind(PluginDescription.class).toInstance(description);
binder.bind(PluginManager.class).toInstance(server.getPluginManager());
binder.bind(EventManager.class).toInstance(server.getEventManager());
binder.bind(CommandManager.class).toInstance(server.getCommandManager());
}
}

View File

@@ -8,96 +8,107 @@ import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.*;
public class PluginDependencyUtils {
private PluginDependencyUtils() {
throw new AssertionError();
private PluginDependencyUtils() {
throw new AssertionError();
}
public static List<PluginDescription> sortCandidates(
List<@NonNull PluginDescription> candidates) {
// Create our graph, we're going to be using this for Kahn's algorithm.
MutableGraph<PluginDescription> graph = GraphBuilder.directed().allowsSelfLoops(false).build();
Map<String, PluginDescription> candidateMap = Maps
.uniqueIndex(candidates, d -> d == null ? null : d.getId());
// Add edges
for (PluginDescription description : candidates) {
graph.addNode(description);
for (PluginDependency dependency : description.getDependencies()) {
PluginDescription in = candidateMap.get(dependency.getId());
if (in != null) {
graph.putEdge(description, in);
}
}
}
public static List<PluginDescription> sortCandidates(List<@NonNull PluginDescription> candidates) {
// Create our graph, we're going to be using this for Kahn's algorithm.
MutableGraph<PluginDescription> graph = GraphBuilder.directed().allowsSelfLoops(false).build();
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(candidates, d -> d == null ? null : d.getId());
// Find nodes that have no edges
Queue<PluginDescription> noEdges = getNoDependencyCandidates(graph);
// Add edges
for (PluginDescription description : candidates) {
graph.addNode(description);
// Actually run Kahn's algorithm
List<PluginDescription> sorted = new ArrayList<>();
while (!noEdges.isEmpty()) {
PluginDescription candidate = noEdges.remove();
sorted.add(candidate);
for (PluginDependency dependency : description.getDependencies()) {
PluginDescription in = candidateMap.get(dependency.getId());
for (PluginDescription node : ImmutableSet.copyOf(graph.adjacentNodes(candidate))) {
graph.removeEdge(node, candidate);
if (in != null) {
graph.putEdge(description, in);
}
}
if (graph.adjacentNodes(node).isEmpty()) {
noEdges.add(node);
}
// Find nodes that have no edges
Queue<PluginDescription> noEdges = getNoDependencyCandidates(graph);
// Actually run Kahn's algorithm
List<PluginDescription> sorted = new ArrayList<>();
while (!noEdges.isEmpty()) {
PluginDescription candidate = noEdges.remove();
sorted.add(candidate);
for (PluginDescription node : ImmutableSet.copyOf(graph.adjacentNodes(candidate))) {
graph.removeEdge(node, candidate);
if (graph.adjacentNodes(node).isEmpty()) {
noEdges.add(node);
}
}
}
if (!graph.edges().isEmpty()) {
throw new IllegalStateException("Plugin circular dependency found: " + createLoopInformation(graph));
}
return sorted;
}
}
public static Queue<PluginDescription> getNoDependencyCandidates(Graph<PluginDescription> graph) {
Queue<PluginDescription> found = new ArrayDeque<>();
for (PluginDescription node : graph.nodes()) {
if (graph.outDegree(node) == 0) {
found.add(node);
}
}
return found;
if (!graph.edges().isEmpty()) {
throw new IllegalStateException(
"Plugin circular dependency found: " + createLoopInformation(graph));
}
public static String createLoopInformation(Graph<PluginDescription> graph) {
StringBuilder repr = new StringBuilder("{");
for (EndpointPair<PluginDescription> edge : graph.edges()) {
repr.append(edge.target().getId()).append(": [");
repr.append(dependencyLoopInfo(graph, edge.target(), new HashSet<>())).append("], ");
}
repr.setLength(repr.length() - 2);
repr.append("}");
return repr.toString();
return sorted;
}
public static Queue<PluginDescription> getNoDependencyCandidates(Graph<PluginDescription> graph) {
Queue<PluginDescription> found = new ArrayDeque<>();
for (PluginDescription node : graph.nodes()) {
if (graph.outDegree(node) == 0) {
found.add(node);
}
}
private static String dependencyLoopInfo(Graph<PluginDescription> graph, PluginDescription dependency, Set<PluginDescription> seen) {
StringBuilder repr = new StringBuilder();
for (PluginDescription pd : graph.adjacentNodes(dependency)) {
if (seen.add(pd)) {
repr.append(pd.getId()).append(": [").append(dependencyLoopInfo(graph, dependency, seen)).append("], ");
} else {
repr.append(pd.getId()).append(", ");
}
}
return found;
}
if (repr.length() != 0) {
repr.setLength(repr.length() - 2);
return repr.toString();
} else {
return "<no depends>";
}
public static String createLoopInformation(Graph<PluginDescription> graph) {
StringBuilder repr = new StringBuilder("{");
for (EndpointPair<PluginDescription> edge : graph.edges()) {
repr.append(edge.target().getId()).append(": [");
repr.append(dependencyLoopInfo(graph, edge.target(), new HashSet<>())).append("], ");
}
repr.setLength(repr.length() - 2);
repr.append("}");
return repr.toString();
}
private static String dependencyLoopInfo(Graph<PluginDescription> graph,
PluginDescription dependency, Set<PluginDescription> seen) {
StringBuilder repr = new StringBuilder();
for (PluginDescription pd : graph.adjacentNodes(dependency)) {
if (seen.add(pd)) {
repr.append(pd.getId()).append(": [").append(dependencyLoopInfo(graph, dependency, seen))
.append("], ");
} else {
repr.append(pd.getId()).append(", ");
}
}
if (repr.length() != 0) {
repr.setLength(repr.length() - 2);
return repr.toString();
} else {
return "<no depends>";
}
}
}

View File

@@ -4,9 +4,10 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import io.netty.buffer.ByteBuf;
public interface MinecraftPacket {
void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion);
void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion);
void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion);
boolean handle(MinecraftSessionHandler handler);
void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion);
boolean handle(MinecraftSessionHandler handler);
}

View File

@@ -2,56 +2,59 @@ package com.velocitypowered.proxy.protocol;
import com.google.common.primitives.ImmutableIntArray;
public enum ProtocolConstants { ;
public static final int LEGACY = -1;
public enum ProtocolConstants {
;
public static final int LEGACY = -1;
public static final int MINECRAFT_1_8 = 47;
public static final int MINECRAFT_1_9 = 107;
public static final int MINECRAFT_1_9_1 = 108;
public static final int MINECRAFT_1_9_2 = 109;
public static final int MINECRAFT_1_9_4 = 110;
public static final int MINECRAFT_1_10 = 210;
public static final int MINECRAFT_1_11 = 315;
public static final int MINECRAFT_1_11_1 = 316;
public static final int MINECRAFT_1_12 = 335;
public static final int MINECRAFT_1_12_1 = 338;
public static final int MINECRAFT_1_12_2 = 340;
public static final int MINECRAFT_1_13 = 393;
public static final int MINECRAFT_1_13_1 = 401;
public static final int MINECRAFT_1_13_2 = 404;
public static final int MINECRAFT_1_8 = 47;
public static final int MINECRAFT_1_9 = 107;
public static final int MINECRAFT_1_9_1 = 108;
public static final int MINECRAFT_1_9_2 = 109;
public static final int MINECRAFT_1_9_4 = 110;
public static final int MINECRAFT_1_10 = 210;
public static final int MINECRAFT_1_11 = 315;
public static final int MINECRAFT_1_11_1 = 316;
public static final int MINECRAFT_1_12 = 335;
public static final int MINECRAFT_1_12_1 = 338;
public static final int MINECRAFT_1_12_2 = 340;
public static final int MINECRAFT_1_13 = 393;
public static final int MINECRAFT_1_13_1 = 401;
public static final int MINECRAFT_1_13_2 = 404;
public static final int MINIMUM_GENERIC_VERSION = MINECRAFT_1_8;
public static final int MAXIMUM_GENERIC_VERSION = MINECRAFT_1_13_2;
public static final int MINIMUM_GENERIC_VERSION = MINECRAFT_1_8;
public static final int MAXIMUM_GENERIC_VERSION = MINECRAFT_1_13_2;
public static final String SUPPORTED_GENERIC_VERSION_STRING = "1.8-1.13.2";
public static final String SUPPORTED_GENERIC_VERSION_STRING = "1.8-1.13.2";
public static final ImmutableIntArray SUPPORTED_VERSIONS = ImmutableIntArray.of(
MINECRAFT_1_8,
MINECRAFT_1_9,
MINECRAFT_1_9_1,
MINECRAFT_1_9_2,
MINECRAFT_1_9_4,
MINECRAFT_1_10,
MINECRAFT_1_11,
MINECRAFT_1_11_1,
MINECRAFT_1_12,
MINECRAFT_1_12_1,
MINECRAFT_1_12_2,
MINECRAFT_1_13,
MINECRAFT_1_13_1,
MINECRAFT_1_13_2
);
public static final ImmutableIntArray SUPPORTED_VERSIONS = ImmutableIntArray.of(
MINECRAFT_1_8,
MINECRAFT_1_9,
MINECRAFT_1_9_1,
MINECRAFT_1_9_2,
MINECRAFT_1_9_4,
MINECRAFT_1_10,
MINECRAFT_1_11,
MINECRAFT_1_11_1,
MINECRAFT_1_12,
MINECRAFT_1_12_1,
MINECRAFT_1_12_2,
MINECRAFT_1_13,
MINECRAFT_1_13_1,
MINECRAFT_1_13_2
);
public static boolean isSupported(int version) {
return SUPPORTED_VERSIONS.contains(version);
}
public enum Direction {
SERVERBOUND,
CLIENTBOUND;
public StateRegistry.PacketRegistry.ProtocolVersion getProtocol(StateRegistry state, int protocolVersion) {
return (this == SERVERBOUND ? state.SERVERBOUND : state.CLIENTBOUND).getVersion(protocolVersion);
}
public static boolean isSupported(int version) {
return SUPPORTED_VERSIONS.contains(version);
}
public enum Direction {
SERVERBOUND,
CLIENTBOUND;
public StateRegistry.PacketRegistry.ProtocolVersion getProtocol(StateRegistry state,
int protocolVersion) {
return (this == SERVERBOUND ? state.SERVERBOUND : state.CLIENTBOUND)
.getVersion(protocolVersion);
}
}
}

View File

@@ -4,113 +4,119 @@ import com.google.common.base.Preconditions;
import com.velocitypowered.api.util.GameProfile;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public enum ProtocolUtils { ;
private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
public enum ProtocolUtils {
;
private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
public static int readVarInt(ByteBuf buf) {
int i = 0;
int j = 0;
while (true) {
int k = buf.readByte();
i |= (k & 0x7F) << j++ * 7;
if (j > 5) throw new RuntimeException("VarInt too big");
if ((k & 0x80) != 128) break;
}
return i;
public static int readVarInt(ByteBuf buf) {
int i = 0;
int j = 0;
while (true) {
int k = buf.readByte();
i |= (k & 0x7F) << j++ * 7;
if (j > 5) {
throw new RuntimeException("VarInt too big");
}
if ((k & 0x80) != 128) {
break;
}
}
return i;
}
public static void writeVarInt(ByteBuf buf, int value) {
while (true) {
if ((value & 0xFFFFFF80) == 0) {
buf.writeByte(value);
return;
}
public static void writeVarInt(ByteBuf buf, int value) {
while (true) {
if ((value & 0xFFFFFF80) == 0) {
buf.writeByte(value);
return;
}
buf.writeByte(value & 0x7F | 0x80);
value >>>= 7;
}
buf.writeByte(value & 0x7F | 0x80);
value >>>= 7;
}
}
public static String readString(ByteBuf buf) {
return readString(buf, DEFAULT_MAX_STRING_SIZE);
}
public static String readString(ByteBuf buf) {
return readString(buf, DEFAULT_MAX_STRING_SIZE);
}
public static String readString(ByteBuf buf, int cap) {
int length = readVarInt(buf);
Preconditions.checkArgument(length <= cap, "Bad string size (got %s, maximum is %s)", length, cap);
String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.skipBytes(length);
return str;
}
public static String readString(ByteBuf buf, int cap) {
int length = readVarInt(buf);
Preconditions
.checkArgument(length <= cap, "Bad string size (got %s, maximum is %s)", length, cap);
String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.skipBytes(length);
return str;
}
public static void writeString(ByteBuf buf, String str) {
int size = ByteBufUtil.utf8Bytes(str);
writeVarInt(buf, size);
ByteBufUtil.writeUtf8(buf, str);
}
public static void writeString(ByteBuf buf, String str) {
int size = ByteBufUtil.utf8Bytes(str);
writeVarInt(buf, size);
ByteBufUtil.writeUtf8(buf, str);
}
public static byte[] readByteArray(ByteBuf buf) {
return readByteArray(buf, DEFAULT_MAX_STRING_SIZE);
}
public static byte[] readByteArray(ByteBuf buf) {
return readByteArray(buf, DEFAULT_MAX_STRING_SIZE);
}
public static byte[] readByteArray(ByteBuf buf, int cap) {
int length = readVarInt(buf);
Preconditions.checkArgument(length <= cap, "Bad string size (got %s, maximum is %s)", length, cap);
byte[] array = new byte[length];
buf.readBytes(array);
return array;
}
public static byte[] readByteArray(ByteBuf buf, int cap) {
int length = readVarInt(buf);
Preconditions
.checkArgument(length <= cap, "Bad string size (got %s, maximum is %s)", length, cap);
byte[] array = new byte[length];
buf.readBytes(array);
return array;
}
public static void writeByteArray(ByteBuf buf, byte[] array) {
writeVarInt(buf, array.length);
buf.writeBytes(array);
}
public static void writeByteArray(ByteBuf buf, byte[] array) {
writeVarInt(buf, array.length);
buf.writeBytes(array);
}
public static UUID readUuid(ByteBuf buf) {
long msb = buf.readLong();
long lsb = buf.readLong();
return new UUID(msb, lsb);
}
public static UUID readUuid(ByteBuf buf) {
long msb = buf.readLong();
long lsb = buf.readLong();
return new UUID(msb, lsb);
}
public static void writeUuid(ByteBuf buf, UUID uuid) {
buf.writeLong(uuid.getMostSignificantBits());
buf.writeLong(uuid.getLeastSignificantBits());
}
public static void writeUuid(ByteBuf buf, UUID uuid) {
buf.writeLong(uuid.getMostSignificantBits());
buf.writeLong(uuid.getLeastSignificantBits());
}
public static void writeProperties(ByteBuf buf, List<GameProfile.Property> properties) {
writeVarInt(buf, properties.size());
for (GameProfile.Property property : properties) {
writeString(buf, property.getName());
writeString(buf, property.getValue());
String signature = property.getSignature();
if (signature != null) {
buf.writeBoolean(true);
writeString(buf, signature);
} else {
buf.writeBoolean(false);
}
}
public static void writeProperties(ByteBuf buf, List<GameProfile.Property> properties) {
writeVarInt(buf, properties.size());
for (GameProfile.Property property : properties) {
writeString(buf, property.getName());
writeString(buf, property.getValue());
String signature = property.getSignature();
if (signature != null) {
buf.writeBoolean(true);
writeString(buf, signature);
} else {
buf.writeBoolean(false);
}
}
}
public static List<GameProfile.Property> readProperties(ByteBuf buf) {
List<GameProfile.Property> properties = new ArrayList<>();
int size = readVarInt(buf);
for (int i = 0; i < size; i++) {
String name = readString(buf);
String value = readString(buf);
String signature = "";
boolean hasSignature = buf.readBoolean();
if (hasSignature) {
signature = readString(buf);
}
properties.add(new GameProfile.Property(name, value, signature));
}
return properties;
public static List<GameProfile.Property> readProperties(ByteBuf buf) {
List<GameProfile.Property> properties = new ArrayList<>();
int size = readVarInt(buf);
for (int i = 0; i < size; i++) {
String name = readString(buf);
String value = readString(buf);
String signature = "";
boolean hasSignature = buf.readBoolean();
if (hasSignature) {
signature = readString(buf);
}
properties.add(new GameProfile.Property(name, value, signature));
}
return properties;
}
}

View File

@@ -1,305 +1,362 @@
package com.velocitypowered.proxy.protocol;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.Direction;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_10;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_11;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_11_1;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_1;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_2;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_13;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_13_1;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_13_2;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_8;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_9;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_9_1;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_9_2;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_9_4;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINIMUM_GENERIC_VERSION;
import com.google.common.primitives.ImmutableIntArray;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.protocol.packet.BossBar;
import com.velocitypowered.proxy.protocol.packet.Chat;
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequest;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponse;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter;
import com.velocitypowered.proxy.protocol.packet.JoinGame;
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage;
import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse;
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.Respawn;
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess;
import com.velocitypowered.proxy.protocol.packet.SetCompression;
import com.velocitypowered.proxy.protocol.packet.StatusPing;
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
import com.velocitypowered.proxy.protocol.packet.TitlePacket;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Objects;
import java.util.function.Supplier;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.*;
import org.checkerframework.checker.nullness.qual.Nullable;
public enum StateRegistry {
HANDSHAKE {
{
SERVERBOUND.register(Handshake.class, Handshake::new,
genericMappings(0x00));
}
},
STATUS {
{
SERVERBOUND.register(StatusRequest.class, () -> StatusRequest.INSTANCE,
genericMappings(0x00));
SERVERBOUND.register(StatusPing.class, StatusPing::new,
genericMappings(0x01));
CLIENTBOUND.register(StatusResponse.class, StatusResponse::new,
genericMappings(0x00));
CLIENTBOUND.register(StatusPing.class, StatusPing::new,
genericMappings(0x01));
}
},
PLAY {
{
SERVERBOUND.fallback = false;
CLIENTBOUND.fallback = false;
HANDSHAKE {
{
SERVERBOUND.register(Handshake.class, Handshake::new,
genericMappings(0x00));
}
},
STATUS {
{
SERVERBOUND.register(StatusRequest.class, () -> StatusRequest.INSTANCE,
genericMappings(0x00));
SERVERBOUND.register(StatusPing.class, StatusPing::new,
genericMappings(0x01));
SERVERBOUND.register(TabCompleteRequest.class, TabCompleteRequest::new,
map(0x14, MINECRAFT_1_8, false),
map(0x01, MINECRAFT_1_9, false),
map(0x02, MINECRAFT_1_12, false),
map(0x01, MINECRAFT_1_12_1, false));
SERVERBOUND.register(Chat.class, Chat::new,
map(0x01, MINECRAFT_1_8, false),
map(0x02, MINECRAFT_1_9, false),
map(0x03, MINECRAFT_1_12, false),
map(0x02, MINECRAFT_1_12_1, false),
map(0x02, MINECRAFT_1_13, false));
SERVERBOUND.register(ClientSettings.class, ClientSettings::new,
map(0x15, MINECRAFT_1_8, false),
map(0x04, MINECRAFT_1_9, false),
map(0x05, MINECRAFT_1_12, false),
map(0x04, MINECRAFT_1_12_1, false),
map(0x04, MINECRAFT_1_13, false));
SERVERBOUND.register(PluginMessage.class, PluginMessage::new,
map(0x17, MINECRAFT_1_8, false),
map(0x09, MINECRAFT_1_9, false),
map(0x0A, MINECRAFT_1_12, false),
map(0x09, MINECRAFT_1_12_1, false),
map(0x0A, MINECRAFT_1_13, false));
SERVERBOUND.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_8, false),
map(0x0B, MINECRAFT_1_9, false),
map(0x0C, MINECRAFT_1_12, false),
map(0x0B, MINECRAFT_1_12_1, false),
map(0x0E, MINECRAFT_1_13, false));
CLIENTBOUND.register(StatusResponse.class, StatusResponse::new,
genericMappings(0x00));
CLIENTBOUND.register(StatusPing.class, StatusPing::new,
genericMappings(0x01));
}
},
PLAY {
{
SERVERBOUND.fallback = false;
CLIENTBOUND.fallback = false;
CLIENTBOUND.register(BossBar.class, BossBar::new,
map(0x0C, MINECRAFT_1_9, false),
map(0x0C, MINECRAFT_1_12, false),
map(0x0C, MINECRAFT_1_13, false));
CLIENTBOUND.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_8, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0F, MINECRAFT_1_12, true),
map(0x0E, MINECRAFT_1_13, true));
CLIENTBOUND.register(TabCompleteResponse.class, TabCompleteResponse::new,
map(0x3A, MINECRAFT_1_8, false),
map(0x0E, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_12, false));
CLIENTBOUND.register(PluginMessage.class, PluginMessage::new,
map(0x3F, MINECRAFT_1_8, false),
map(0x18, MINECRAFT_1_9, false),
map(0x18, MINECRAFT_1_12, false),
map(0x19, MINECRAFT_1_13, false));
CLIENTBOUND.register(Disconnect.class, Disconnect::new,
map(0x40, MINECRAFT_1_8, false),
map(0x1A, MINECRAFT_1_9, false),
map(0x1A, MINECRAFT_1_12, false),
map(0x1B, MINECRAFT_1_13, false));
CLIENTBOUND.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_8, false),
map(0x1F, MINECRAFT_1_9, false),
map(0x1F, MINECRAFT_1_12, false),
map(0x21, MINECRAFT_1_13, false));
CLIENTBOUND.register(JoinGame.class, JoinGame::new,
map(0x01, MINECRAFT_1_8, false),
map(0x23, MINECRAFT_1_9, false),
map(0x23, MINECRAFT_1_12, false),
map(0x25, MINECRAFT_1_13, false));
CLIENTBOUND.register(Respawn.class, Respawn::new,
map(0x07, MINECRAFT_1_8, true),
map(0x33, MINECRAFT_1_9, true),
map(0x34, MINECRAFT_1_12, true),
map(0x35, MINECRAFT_1_12_2, true),
map(0x38, MINECRAFT_1_13, true));
CLIENTBOUND.register(HeaderAndFooter.class, HeaderAndFooter::new,
map(0x47, MINECRAFT_1_8, true),
map(0x48, MINECRAFT_1_9, true),
map(0x47, MINECRAFT_1_9_4, true),
map(0x49, MINECRAFT_1_12, true),
map(0x4A, MINECRAFT_1_12_1, true),
map(0x4E, MINECRAFT_1_13, true));
CLIENTBOUND.register(TitlePacket.class, TitlePacket::new,
map(0x45, MINECRAFT_1_8, true),
map(0x45, MINECRAFT_1_9, true),
map(0x47, MINECRAFT_1_12, true),
map(0x48, MINECRAFT_1_12_1, true),
map(0x4B, MINECRAFT_1_13, true));
CLIENTBOUND.register(PlayerListItem.class, PlayerListItem::new,
map(0x38, MINECRAFT_1_8, false),
map(0x2D, MINECRAFT_1_9, false),
map(0x2D, MINECRAFT_1_12, false),
map(0x2E, MINECRAFT_1_12_1, false),
map(0x30, MINECRAFT_1_13, false));
}
},
LOGIN {
{
SERVERBOUND.register(ServerLogin.class, ServerLogin::new,
genericMappings(0x00));
SERVERBOUND.register(EncryptionResponse.class, EncryptionResponse::new,
genericMappings(0x01));
SERVERBOUND.register(LoginPluginResponse.class, LoginPluginResponse::new,
map(0x02, MINECRAFT_1_13, false));
SERVERBOUND.register(TabCompleteRequest.class, TabCompleteRequest::new,
map(0x14, MINECRAFT_1_8, false),
map(0x01, MINECRAFT_1_9, false),
map(0x02, MINECRAFT_1_12, false),
map(0x01, MINECRAFT_1_12_1, false));
SERVERBOUND.register(Chat.class, Chat::new,
map(0x01, MINECRAFT_1_8, false),
map(0x02, MINECRAFT_1_9, false),
map(0x03, MINECRAFT_1_12, false),
map(0x02, MINECRAFT_1_12_1, false),
map(0x02, MINECRAFT_1_13, false));
SERVERBOUND.register(ClientSettings.class, ClientSettings::new,
map(0x15, MINECRAFT_1_8, false),
map(0x04, MINECRAFT_1_9, false),
map(0x05, MINECRAFT_1_12, false),
map(0x04, MINECRAFT_1_12_1, false),
map(0x04, MINECRAFT_1_13, false));
SERVERBOUND.register(PluginMessage.class, PluginMessage::new,
map(0x17, MINECRAFT_1_8, false),
map(0x09, MINECRAFT_1_9, false),
map(0x0A, MINECRAFT_1_12, false),
map(0x09, MINECRAFT_1_12_1, false),
map(0x0A, MINECRAFT_1_13, false));
SERVERBOUND.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_8, false),
map(0x0B, MINECRAFT_1_9, false),
map(0x0C, MINECRAFT_1_12, false),
map(0x0B, MINECRAFT_1_12_1, false),
map(0x0E, MINECRAFT_1_13, false));
CLIENTBOUND.register(Disconnect.class, Disconnect::new,
genericMappings(0x00));
CLIENTBOUND.register(EncryptionRequest.class, EncryptionRequest::new,
genericMappings(0x01));
CLIENTBOUND.register(ServerLoginSuccess.class, ServerLoginSuccess::new,
genericMappings(0x02));
CLIENTBOUND.register(SetCompression.class, SetCompression::new,
genericMappings(0x03));
CLIENTBOUND.register(LoginPluginMessage.class, LoginPluginMessage::new,
map(0x04, MINECRAFT_1_13, false));
CLIENTBOUND.register(BossBar.class, BossBar::new,
map(0x0C, MINECRAFT_1_9, false),
map(0x0C, MINECRAFT_1_12, false),
map(0x0C, MINECRAFT_1_13, false));
CLIENTBOUND.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_8, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0F, MINECRAFT_1_12, true),
map(0x0E, MINECRAFT_1_13, true));
CLIENTBOUND.register(TabCompleteResponse.class, TabCompleteResponse::new,
map(0x3A, MINECRAFT_1_8, false),
map(0x0E, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_12, false));
CLIENTBOUND.register(PluginMessage.class, PluginMessage::new,
map(0x3F, MINECRAFT_1_8, false),
map(0x18, MINECRAFT_1_9, false),
map(0x18, MINECRAFT_1_12, false),
map(0x19, MINECRAFT_1_13, false));
CLIENTBOUND.register(Disconnect.class, Disconnect::new,
map(0x40, MINECRAFT_1_8, false),
map(0x1A, MINECRAFT_1_9, false),
map(0x1A, MINECRAFT_1_12, false),
map(0x1B, MINECRAFT_1_13, false));
CLIENTBOUND.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_8, false),
map(0x1F, MINECRAFT_1_9, false),
map(0x1F, MINECRAFT_1_12, false),
map(0x21, MINECRAFT_1_13, false));
CLIENTBOUND.register(JoinGame.class, JoinGame::new,
map(0x01, MINECRAFT_1_8, false),
map(0x23, MINECRAFT_1_9, false),
map(0x23, MINECRAFT_1_12, false),
map(0x25, MINECRAFT_1_13, false));
CLIENTBOUND.register(Respawn.class, Respawn::new,
map(0x07, MINECRAFT_1_8, true),
map(0x33, MINECRAFT_1_9, true),
map(0x34, MINECRAFT_1_12, true),
map(0x35, MINECRAFT_1_12_2, true),
map(0x38, MINECRAFT_1_13, true));
CLIENTBOUND.register(HeaderAndFooter.class, HeaderAndFooter::new,
map(0x47, MINECRAFT_1_8, true),
map(0x48, MINECRAFT_1_9, true),
map(0x47, MINECRAFT_1_9_4, true),
map(0x49, MINECRAFT_1_12, true),
map(0x4A, MINECRAFT_1_12_1, true),
map(0x4E, MINECRAFT_1_13, true));
CLIENTBOUND.register(TitlePacket.class, TitlePacket::new,
map(0x45, MINECRAFT_1_8, true),
map(0x45, MINECRAFT_1_9, true),
map(0x47, MINECRAFT_1_12, true),
map(0x48, MINECRAFT_1_12_1, true),
map(0x4B, MINECRAFT_1_13, true));
CLIENTBOUND.register(PlayerListItem.class, PlayerListItem::new,
map(0x38, MINECRAFT_1_8, false),
map(0x2D, MINECRAFT_1_9, false),
map(0x2D, MINECRAFT_1_12, false),
map(0x2E, MINECRAFT_1_12_1, false),
map(0x30, MINECRAFT_1_13, false));
}
},
LOGIN {
{
SERVERBOUND.register(ServerLogin.class, ServerLogin::new,
genericMappings(0x00));
SERVERBOUND.register(EncryptionResponse.class, EncryptionResponse::new,
genericMappings(0x01));
SERVERBOUND.register(LoginPluginResponse.class, LoginPluginResponse::new,
map(0x02, MINECRAFT_1_13, false));
CLIENTBOUND.register(Disconnect.class, Disconnect::new,
genericMappings(0x00));
CLIENTBOUND.register(EncryptionRequest.class, EncryptionRequest::new,
genericMappings(0x01));
CLIENTBOUND.register(ServerLoginSuccess.class, ServerLoginSuccess::new,
genericMappings(0x02));
CLIENTBOUND.register(SetCompression.class, SetCompression::new,
genericMappings(0x03));
CLIENTBOUND.register(LoginPluginMessage.class, LoginPluginMessage::new,
map(0x04, MINECRAFT_1_13, false));
}
};
public static final int STATUS_ID = 1;
public static final int LOGIN_ID = 2;
public final PacketRegistry CLIENTBOUND = new PacketRegistry(
ProtocolConstants.Direction.CLIENTBOUND);
public final PacketRegistry SERVERBOUND = new PacketRegistry(
ProtocolConstants.Direction.SERVERBOUND);
public static class PacketRegistry {
private static final IntObjectMap<ImmutableIntArray> LINKED_PROTOCOL_VERSIONS = new IntObjectHashMap<>();
static {
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_9,
ImmutableIntArray.of(MINECRAFT_1_9_1, MINECRAFT_1_9_2, MINECRAFT_1_9_4));
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_9_4,
ImmutableIntArray.of(MINECRAFT_1_10, MINECRAFT_1_11, MINECRAFT_1_11_1));
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_12, ImmutableIntArray.of(MINECRAFT_1_12_1));
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_12_1, ImmutableIntArray.of(MINECRAFT_1_12_2));
LINKED_PROTOCOL_VERSIONS
.put(MINECRAFT_1_13, ImmutableIntArray.of(MINECRAFT_1_13_1, MINECRAFT_1_13_2));
}
private final ProtocolConstants.Direction direction;
private final IntObjectMap<ProtocolVersion> versions = new IntObjectHashMap<>(16);
private boolean fallback = true;
public PacketRegistry(Direction direction) {
this.direction = direction;
ProtocolConstants.SUPPORTED_VERSIONS
.forEach(version -> versions.put(version, new ProtocolVersion(version)));
}
public ProtocolVersion getVersion(final int version) {
ProtocolVersion result = versions.get(version);
if (result == null) {
if (fallback) {
return getVersion(MINIMUM_GENERIC_VERSION);
}
throw new IllegalArgumentException("Could not find data for protocol version " + version);
}
return result;
}
public <P extends MinecraftPacket> void register(Class<P> clazz, Supplier<P> packetSupplier,
PacketMapping... mappings) {
if (mappings.length == 0) {
throw new IllegalArgumentException("At least one mapping must be provided.");
}
for (final PacketMapping mapping : mappings) {
ProtocolVersion version = this.versions.get(mapping.protocolVersion);
if (version == null) {
throw new IllegalArgumentException("Unknown protocol version " + mapping.protocolVersion);
}
if (!mapping.encodeOnly) {
version.packetIdToSupplier.put(mapping.id, packetSupplier);
}
version.packetClassToId.put(clazz, mapping.id);
ImmutableIntArray linked = LINKED_PROTOCOL_VERSIONS.get(mapping.protocolVersion);
if (linked != null) {
links:
for (int i = 0; i < linked.length(); i++) {
int linkedVersion = linked.get(i);
// Make sure that later mappings override this one.
for (PacketMapping m : mappings) {
if (linkedVersion == m.protocolVersion) {
continue links;
}
}
register(clazz, packetSupplier, map(mapping.id, linkedVersion, mapping.encodeOnly));
}
}
}
}
public class ProtocolVersion {
public final int version;
final IntObjectMap<Supplier<? extends MinecraftPacket>> packetIdToSupplier = new IntObjectHashMap<>(
16, 0.5f);
final Object2IntMap<Class<? extends MinecraftPacket>> packetClassToId = new Object2IntOpenHashMap<>(
16, 0.5f);
ProtocolVersion(final int version) {
this.version = version;
this.packetClassToId.defaultReturnValue(Integer.MIN_VALUE);
}
public @Nullable MinecraftPacket createPacket(final int id) {
final Supplier<? extends MinecraftPacket> supplier = this.packetIdToSupplier.get(id);
if (supplier == null) {
return null;
}
return supplier.get();
}
public int getPacketId(final MinecraftPacket packet) {
final int id = this.packetClassToId.getInt(packet.getClass());
if (id == Integer.MIN_VALUE) {
throw new IllegalArgumentException(String.format(
"Unable to find id for packet of type %s in %s protocol %s",
packet.getClass().getName(), PacketRegistry.this.direction, this.version
));
}
return id;
}
}
}
public static class PacketMapping {
private final int id;
private final int protocolVersion;
private final boolean encodeOnly;
public PacketMapping(int id, int protocolVersion, boolean packetDecoding) {
this.id = id;
this.protocolVersion = protocolVersion;
this.encodeOnly = packetDecoding;
}
@Override
public String toString() {
return "PacketMapping{" +
"id=" + id +
", protocolVersion=" + protocolVersion +
", encodeOnly=" + encodeOnly +
'}';
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PacketMapping that = (PacketMapping) o;
return id == that.id &&
protocolVersion == that.protocolVersion &&
encodeOnly == that.encodeOnly;
}
@Override
public int hashCode() {
return Objects.hash(id, protocolVersion, encodeOnly);
}
}
/**
* Creates a PacketMapping using the provided arguments
*
* @param id Packet Id
* @param version Protocol version
* @param encodeOnly When true packet decoding will be disabled
* @return PacketMapping with the provided arguments
*/
private static PacketMapping map(int id, int version, boolean encodeOnly) {
return new PacketMapping(id, version, encodeOnly);
}
private static PacketMapping[] genericMappings(int id) {
return new PacketMapping[]{
map(id, MINECRAFT_1_8, false),
map(id, MINECRAFT_1_9, false),
map(id, MINECRAFT_1_12, false),
map(id, MINECRAFT_1_13, false)
};
public static final int STATUS_ID = 1;
public static final int LOGIN_ID = 2;
public final PacketRegistry CLIENTBOUND = new PacketRegistry(ProtocolConstants.Direction.CLIENTBOUND);
public final PacketRegistry SERVERBOUND = new PacketRegistry(ProtocolConstants.Direction.SERVERBOUND);
public static class PacketRegistry {
private static final IntObjectMap<ImmutableIntArray> LINKED_PROTOCOL_VERSIONS = new IntObjectHashMap<>();
static {
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_9, ImmutableIntArray.of(MINECRAFT_1_9_1, MINECRAFT_1_9_2, MINECRAFT_1_9_4));
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_9_4, ImmutableIntArray.of(MINECRAFT_1_10, MINECRAFT_1_11, MINECRAFT_1_11_1));
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_12, ImmutableIntArray.of(MINECRAFT_1_12_1));
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_12_1, ImmutableIntArray.of(MINECRAFT_1_12_2));
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_13, ImmutableIntArray.of(MINECRAFT_1_13_1, MINECRAFT_1_13_2));
}
private final ProtocolConstants.Direction direction;
private final IntObjectMap<ProtocolVersion> versions = new IntObjectHashMap<>(16);
private boolean fallback = true;
public PacketRegistry(Direction direction) {
this.direction = direction;
ProtocolConstants.SUPPORTED_VERSIONS.forEach(version -> versions.put(version, new ProtocolVersion(version)));
}
public ProtocolVersion getVersion(final int version) {
ProtocolVersion result = versions.get(version);
if (result == null) {
if (fallback) {
return getVersion(MINIMUM_GENERIC_VERSION);
}
throw new IllegalArgumentException("Could not find data for protocol version " + version);
}
return result;
}
public <P extends MinecraftPacket> void register(Class<P> clazz, Supplier<P> packetSupplier, PacketMapping... mappings) {
if (mappings.length == 0) {
throw new IllegalArgumentException("At least one mapping must be provided.");
}
for (final PacketMapping mapping : mappings) {
ProtocolVersion version = this.versions.get(mapping.protocolVersion);
if (version == null) {
throw new IllegalArgumentException("Unknown protocol version " + mapping.protocolVersion);
}
if (!mapping.encodeOnly) {
version.packetIdToSupplier.put(mapping.id, packetSupplier);
}
version.packetClassToId.put(clazz, mapping.id);
ImmutableIntArray linked = LINKED_PROTOCOL_VERSIONS.get(mapping.protocolVersion);
if (linked != null) {
links: for (int i = 0; i < linked.length(); i++) {
int linkedVersion = linked.get(i);
// Make sure that later mappings override this one.
for (PacketMapping m : mappings) {
if (linkedVersion == m.protocolVersion) continue links;
}
register(clazz, packetSupplier, map(mapping.id, linkedVersion, mapping.encodeOnly));
}
}
}
}
public class ProtocolVersion {
public final int version;
final IntObjectMap<Supplier<? extends MinecraftPacket>> packetIdToSupplier = new IntObjectHashMap<>(16, 0.5f);
final Object2IntMap<Class<? extends MinecraftPacket>> packetClassToId = new Object2IntOpenHashMap<>(16, 0.5f);
ProtocolVersion(final int version) {
this.version = version;
this.packetClassToId.defaultReturnValue(Integer.MIN_VALUE);
}
public @Nullable MinecraftPacket createPacket(final int id) {
final Supplier<? extends MinecraftPacket> supplier = this.packetIdToSupplier.get(id);
if (supplier == null) {
return null;
}
return supplier.get();
}
public int getPacketId(final MinecraftPacket packet) {
final int id = this.packetClassToId.getInt(packet.getClass());
if (id == Integer.MIN_VALUE) {
throw new IllegalArgumentException(String.format(
"Unable to find id for packet of type %s in %s protocol %s",
packet.getClass().getName(), PacketRegistry.this.direction, this.version
));
}
return id;
}
}
}
public static class PacketMapping {
private final int id;
private final int protocolVersion;
private final boolean encodeOnly;
public PacketMapping(int id, int protocolVersion, boolean packetDecoding) {
this.id = id;
this.protocolVersion = protocolVersion;
this.encodeOnly = packetDecoding;
}
@Override
public String toString() {
return "PacketMapping{" +
"id=" + id +
", protocolVersion=" + protocolVersion +
", encodeOnly=" + encodeOnly +
'}';
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PacketMapping that = (PacketMapping) o;
return id == that.id &&
protocolVersion == that.protocolVersion &&
encodeOnly == that.encodeOnly;
}
@Override
public int hashCode() {
return Objects.hash(id, protocolVersion, encodeOnly);
}
}
/**
* Creates a PacketMapping using the provided arguments
* @param id Packet Id
* @param version Protocol version
* @param encodeOnly When true packet decoding will be disabled
* @return PacketMapping with the provided arguments
*/
private static PacketMapping map(int id, int version, boolean encodeOnly) {
return new PacketMapping(id, version, encodeOnly);
}
private static PacketMapping[] genericMappings(int id) {
return new PacketMapping[]{
map(id, MINECRAFT_1_8, false),
map(id, MINECRAFT_1_9, false),
map(id, MINECRAFT_1_12, false),
map(id, MINECRAFT_1_13, false)
};
}
}
}

View File

@@ -1,5 +1,8 @@
package com.velocitypowered.proxy.protocol.netty;
import static com.velocitypowered.api.event.query.ProxyQueryEvent.QueryType.BASIC;
import static com.velocitypowered.api.event.query.ProxyQueryEvent.QueryType.FULL;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableSet;
@@ -13,11 +16,6 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import net.kyori.text.serializer.ComponentSerializers;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
@@ -28,239 +26,254 @@ import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.velocitypowered.api.event.query.ProxyQueryEvent.QueryType.BASIC;
import static com.velocitypowered.api.event.query.ProxyQueryEvent.QueryType.FULL;
import net.kyori.text.serializer.ComponentSerializers;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public class GS4QueryHandler extends SimpleChannelInboundHandler<DatagramPacket> {
private static final Logger logger = LogManager.getLogger(GS4QueryHandler.class);
private static final short QUERY_MAGIC_FIRST = 0xFE;
private static final short QUERY_MAGIC_SECOND = 0xFD;
private static final byte QUERY_TYPE_HANDSHAKE = 0x09;
private static final byte QUERY_TYPE_STAT = 0x00;
private static final byte[] QUERY_RESPONSE_FULL_PADDING = new byte[] { 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, (byte) 0x80, 0x00 };
private static final byte[] QUERY_RESPONSE_FULL_PADDING2 = new byte[] { 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00 };
private static final Logger logger = LogManager.getLogger(GS4QueryHandler.class);
// Contents to add into basic stat response. See ResponseWriter class below
private static final Set<String> QUERY_BASIC_RESPONSE_CONTENTS = ImmutableSet.of(
"hostname",
"gametype",
"map",
"numplayers",
"maxplayers",
"hostport",
"hostip"
);
private static final short QUERY_MAGIC_FIRST = 0xFE;
private static final short QUERY_MAGIC_SECOND = 0xFD;
private static final byte QUERY_TYPE_HANDSHAKE = 0x09;
private static final byte QUERY_TYPE_STAT = 0x00;
private static final byte[] QUERY_RESPONSE_FULL_PADDING = new byte[]{0x73, 0x70, 0x6C, 0x69, 0x74,
0x6E, 0x75, 0x6D, 0x00, (byte) 0x80, 0x00};
private static final byte[] QUERY_RESPONSE_FULL_PADDING2 = new byte[]{0x01, 0x70, 0x6C, 0x61,
0x79, 0x65, 0x72, 0x5F, 0x00, 0x00};
private final Cache<InetAddress, Integer> sessions = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
// Contents to add into basic stat response. See ResponseWriter class below
private static final Set<String> QUERY_BASIC_RESPONSE_CONTENTS = ImmutableSet.of(
"hostname",
"gametype",
"map",
"numplayers",
"maxplayers",
"hostport",
"hostip"
);
@MonotonicNonNull
private volatile List<QueryResponse.PluginInformation> pluginInformationList = null;
private final Cache<InetAddress, Integer> sessions = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
private final VelocityServer server;
@MonotonicNonNull
private volatile List<QueryResponse.PluginInformation> pluginInformationList = null;
public GS4QueryHandler(VelocityServer server) {
this.server = server;
private final VelocityServer server;
public GS4QueryHandler(VelocityServer server) {
this.server = server;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
ByteBuf queryMessage = msg.content();
InetAddress senderAddress = msg.sender().getAddress();
// Allocate buffer for response
ByteBuf queryResponse = ctx.alloc().buffer();
DatagramPacket responsePacket = new DatagramPacket(queryResponse, msg.sender());
try {
// Verify query packet magic
if (queryMessage.readUnsignedByte() != QUERY_MAGIC_FIRST
|| queryMessage.readUnsignedByte() != QUERY_MAGIC_SECOND) {
throw new IllegalStateException("Invalid query packet magic");
}
// Read packet header
short type = queryMessage.readUnsignedByte();
int sessionId = queryMessage.readInt();
switch (type) {
case QUERY_TYPE_HANDSHAKE: {
// Generate new challenge token and put it into the sessions cache
int challengeToken = ThreadLocalRandom.current().nextInt();
sessions.put(senderAddress, challengeToken);
// Respond with challenge token
queryResponse.writeByte(QUERY_TYPE_HANDSHAKE);
queryResponse.writeInt(sessionId);
writeString(queryResponse, Integer.toString(challengeToken));
ctx.writeAndFlush(responsePacket);
break;
}
case QUERY_TYPE_STAT: {
// Check if query was done with session previously generated using a handshake packet
int challengeToken = queryMessage.readInt();
Integer session = sessions.getIfPresent(senderAddress);
if (session == null || session != challengeToken) {
throw new IllegalStateException("Invalid challenge token");
}
// Check which query response client expects
if (queryMessage.readableBytes() != 0 && queryMessage.readableBytes() != 4) {
throw new IllegalStateException("Invalid query packet");
}
// Build query response
QueryResponse response = QueryResponse.builder()
.hostname(ComponentSerializers.PLAIN
.serialize(server.getConfiguration().getMotdComponent()))
.gameVersion(ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING)
.map(server.getConfiguration().getQueryMap())
.currentPlayers(server.getPlayerCount())
.maxPlayers(server.getConfiguration().getShowMaxPlayers())
.proxyPort(server.getConfiguration().getBind().getPort())
.proxyHost(server.getConfiguration().getBind().getHostString())
.players(server.getAllPlayers().stream().map(Player::getUsername)
.collect(Collectors.toList()))
.proxyVersion("Velocity")
.plugins(
server.getConfiguration().shouldQueryShowPlugins() ? getRealPluginInformation()
: Collections.emptyList())
.build();
boolean isBasic = queryMessage.readableBytes() == 0;
// Call event and write response
server.getEventManager()
.fire(new ProxyQueryEvent(isBasic ? BASIC : FULL, senderAddress, response))
.whenCompleteAsync((event, exc) -> {
// Packet header
queryResponse.writeByte(QUERY_TYPE_STAT);
queryResponse.writeInt(sessionId);
// Start writing the response
ResponseWriter responseWriter = new ResponseWriter(queryResponse, isBasic);
responseWriter.write("hostname", event.getResponse().getHostname());
responseWriter.write("gametype", "SMP");
responseWriter.write("game_id", "MINECRAFT");
responseWriter.write("version", event.getResponse().getGameVersion());
responseWriter.writePlugins(event.getResponse().getProxyVersion(),
event.getResponse().getPlugins());
responseWriter.write("map", event.getResponse().getMap());
responseWriter.write("numplayers", event.getResponse().getCurrentPlayers());
responseWriter.write("maxplayers", event.getResponse().getMaxPlayers());
responseWriter.write("hostport", event.getResponse().getProxyPort());
responseWriter.write("hostip", event.getResponse().getProxyHost());
if (!responseWriter.isBasic) {
responseWriter.writePlayers(event.getResponse().getPlayers());
}
// Send the response
ctx.writeAndFlush(responsePacket);
}, ctx.channel().eventLoop());
break;
}
default:
throw new IllegalStateException("Invalid query type: " + type);
}
} catch (Exception e) {
logger.warn("Error while trying to handle a query packet from {}", msg.sender(), e);
// NB: Only need to explicitly release upon exception, writing the response out will decrement the reference
// count.
responsePacket.release();
}
}
private static void writeString(ByteBuf buf, String string) {
buf.writeCharSequence(string, StandardCharsets.ISO_8859_1);
buf.writeByte(0x00);
}
private List<QueryResponse.PluginInformation> getRealPluginInformation() {
// Effective Java, Third Edition; Item 83: Use lazy initialization judiciously
List<QueryResponse.PluginInformation> res = pluginInformationList;
if (res == null) {
synchronized (this) {
if (pluginInformationList == null) {
pluginInformationList = res = server.getPluginManager().getPlugins().stream()
.map(PluginContainer::getDescription)
.map(desc -> QueryResponse.PluginInformation
.of(desc.getName().orElse(desc.getId()), desc.getVersion().orElse(null)))
.collect(Collectors.toList());
}
}
}
return res;
}
private static class ResponseWriter {
private final ByteBuf buf;
private final boolean isBasic;
ResponseWriter(ByteBuf buf, boolean isBasic) {
this.buf = buf;
this.isBasic = isBasic;
if (!isBasic) {
buf.writeBytes(QUERY_RESPONSE_FULL_PADDING);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
ByteBuf queryMessage = msg.content();
InetAddress senderAddress = msg.sender().getAddress();
// Allocate buffer for response
ByteBuf queryResponse = ctx.alloc().buffer();
DatagramPacket responsePacket = new DatagramPacket(queryResponse, msg.sender());
try {
// Verify query packet magic
if (queryMessage.readUnsignedByte() != QUERY_MAGIC_FIRST || queryMessage.readUnsignedByte() != QUERY_MAGIC_SECOND) {
throw new IllegalStateException("Invalid query packet magic");
}
// Read packet header
short type = queryMessage.readUnsignedByte();
int sessionId = queryMessage.readInt();
switch (type) {
case QUERY_TYPE_HANDSHAKE: {
// Generate new challenge token and put it into the sessions cache
int challengeToken = ThreadLocalRandom.current().nextInt();
sessions.put(senderAddress, challengeToken);
// Respond with challenge token
queryResponse.writeByte(QUERY_TYPE_HANDSHAKE);
queryResponse.writeInt(sessionId);
writeString(queryResponse, Integer.toString(challengeToken));
ctx.writeAndFlush(responsePacket);
break;
}
case QUERY_TYPE_STAT: {
// Check if query was done with session previously generated using a handshake packet
int challengeToken = queryMessage.readInt();
Integer session = sessions.getIfPresent(senderAddress);
if (session == null || session != challengeToken) {
throw new IllegalStateException("Invalid challenge token");
}
// Check which query response client expects
if (queryMessage.readableBytes() != 0 && queryMessage.readableBytes() != 4) {
throw new IllegalStateException("Invalid query packet");
}
// Build query response
QueryResponse response = QueryResponse.builder()
.hostname(ComponentSerializers.PLAIN.serialize(server.getConfiguration().getMotdComponent()))
.gameVersion(ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING)
.map(server.getConfiguration().getQueryMap())
.currentPlayers(server.getPlayerCount())
.maxPlayers(server.getConfiguration().getShowMaxPlayers())
.proxyPort(server.getConfiguration().getBind().getPort())
.proxyHost(server.getConfiguration().getBind().getHostString())
.players(server.getAllPlayers().stream().map(Player::getUsername).collect(Collectors.toList()))
.proxyVersion("Velocity")
.plugins(server.getConfiguration().shouldQueryShowPlugins() ? getRealPluginInformation() : Collections.emptyList())
.build();
boolean isBasic = queryMessage.readableBytes() == 0;
// Call event and write response
server.getEventManager().fire(new ProxyQueryEvent(isBasic ? BASIC : FULL, senderAddress, response)).whenCompleteAsync((event, exc) -> {
// Packet header
queryResponse.writeByte(QUERY_TYPE_STAT);
queryResponse.writeInt(sessionId);
// Start writing the response
ResponseWriter responseWriter = new ResponseWriter(queryResponse, isBasic);
responseWriter.write("hostname", event.getResponse().getHostname());
responseWriter.write("gametype", "SMP");
responseWriter.write("game_id", "MINECRAFT");
responseWriter.write("version", event.getResponse().getGameVersion());
responseWriter.writePlugins(event.getResponse().getProxyVersion(), event.getResponse().getPlugins());
responseWriter.write("map", event.getResponse().getMap());
responseWriter.write("numplayers", event.getResponse().getCurrentPlayers());
responseWriter.write("maxplayers", event.getResponse().getMaxPlayers());
responseWriter.write("hostport", event.getResponse().getProxyPort());
responseWriter.write("hostip", event.getResponse().getProxyHost());
if (!responseWriter.isBasic) {
responseWriter.writePlayers(event.getResponse().getPlayers());
}
// Send the response
ctx.writeAndFlush(responsePacket);
}, ctx.channel().eventLoop());
break;
}
default:
throw new IllegalStateException("Invalid query type: " + type);
}
} catch (Exception e) {
logger.warn("Error while trying to handle a query packet from {}", msg.sender(), e);
// NB: Only need to explicitly release upon exception, writing the response out will decrement the reference
// count.
responsePacket.release();
// Writes k/v to stat packet body if this writer is initialized
// for full stat response. Otherwise this follows
// GS4QueryHandler#QUERY_BASIC_RESPONSE_CONTENTS to decide what
// to write into packet body
void write(String key, Object value) {
if (isBasic) {
// Basic contains only specific set of data
if (!QUERY_BASIC_RESPONSE_CONTENTS.contains(key)) {
return;
}
// Special case hostport
if (key.equals("hostport")) {
buf.writeShortLE((Integer) value);
} else {
writeString(buf, value.toString());
}
} else {
writeString(buf, key);
writeString(buf, value.toString());
}
}
private static void writeString(ByteBuf buf, String string) {
buf.writeCharSequence(string, StandardCharsets.ISO_8859_1);
buf.writeByte(0x00);
// Ends packet k/v body writing and writes stat player list to
// the packet if this writer is initialized for full stat response
void writePlayers(Collection<String> players) {
if (isBasic) {
return;
}
// Ends the full stat key-value body with \0
buf.writeByte(0x00);
buf.writeBytes(QUERY_RESPONSE_FULL_PADDING2);
players.forEach(player -> writeString(buf, player));
buf.writeByte(0x00);
}
private List<QueryResponse.PluginInformation> getRealPluginInformation() {
// Effective Java, Third Edition; Item 83: Use lazy initialization judiciously
List<QueryResponse.PluginInformation> res = pluginInformationList;
if (res == null) {
synchronized (this) {
if (pluginInformationList == null) {
pluginInformationList = res = server.getPluginManager().getPlugins().stream()
.map(PluginContainer::getDescription)
.map(desc -> QueryResponse.PluginInformation.of(desc.getName().orElse(desc.getId()), desc.getVersion().orElse(null)))
.collect(Collectors.toList());
}
}
}
return res;
}
private static class ResponseWriter {
private final ByteBuf buf;
private final boolean isBasic;
ResponseWriter(ByteBuf buf, boolean isBasic) {
this.buf = buf;
this.isBasic = isBasic;
if (!isBasic) {
buf.writeBytes(QUERY_RESPONSE_FULL_PADDING);
}
}
// Writes k/v to stat packet body if this writer is initialized
// for full stat response. Otherwise this follows
// GS4QueryHandler#QUERY_BASIC_RESPONSE_CONTENTS to decide what
// to write into packet body
void write(String key, Object value) {
if (isBasic) {
// Basic contains only specific set of data
if (!QUERY_BASIC_RESPONSE_CONTENTS.contains(key)) {
return;
}
// Special case hostport
if (key.equals("hostport")) {
buf.writeShortLE((Integer) value);
} else {
writeString(buf, value.toString());
}
} else {
writeString(buf, key);
writeString(buf, value.toString());
}
}
// Ends packet k/v body writing and writes stat player list to
// the packet if this writer is initialized for full stat response
void writePlayers(Collection<String> players) {
if (isBasic) {
return;
}
// Ends the full stat key-value body with \0
buf.writeByte(0x00);
buf.writeBytes(QUERY_RESPONSE_FULL_PADDING2);
players.forEach(player -> writeString(buf, player));
buf.writeByte(0x00);
}
void writePlugins(String serverVersion, Collection<QueryResponse.PluginInformation> plugins) {
if (isBasic)
return;
StringBuilder pluginsString = new StringBuilder();
pluginsString.append(serverVersion).append(':').append(' ');
Iterator<QueryResponse.PluginInformation> iterator = plugins.iterator();
while (iterator.hasNext()) {
QueryResponse.PluginInformation info = iterator.next();
pluginsString.append(info.getName());
if (info.getVersion() != null) {
pluginsString.append(' ').append(info.getVersion());
}
if (iterator.hasNext()) {
pluginsString.append(';').append(' ');
}
}
writeString(buf, pluginsString.toString());
void writePlugins(String serverVersion, Collection<QueryResponse.PluginInformation> plugins) {
if (isBasic) {
return;
}
StringBuilder pluginsString = new StringBuilder();
pluginsString.append(serverVersion).append(':').append(' ');
Iterator<QueryResponse.PluginInformation> iterator = plugins.iterator();
while (iterator.hasNext()) {
QueryResponse.PluginInformation info = iterator.next();
pluginsString.append(info.getName());
if (info.getVersion() != null) {
pluginsString.append(' ').append(info.getVersion());
}
if (iterator.hasNext()) {
pluginsString.append(';').append(' ');
}
}
writeString(buf, pluginsString.toString());
}
}
}

View File

@@ -5,26 +5,26 @@ import com.velocitypowered.proxy.protocol.packet.LegacyPing;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class LegacyPingDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 2) {
return;
}
short first = in.getUnsignedByte(in.readerIndex());
short second = in.getUnsignedByte(in.readerIndex() + 1);
if (first == 0xfe && second == 0x01) {
in.skipBytes(in.readableBytes());
out.add(new LegacyPing());
} else if (first == 0x02) {
in.skipBytes(in.readableBytes());
out.add(new LegacyHandshake());
} else {
ctx.pipeline().remove(this);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 2) {
return;
}
short first = in.getUnsignedByte(in.readerIndex());
short second = in.getUnsignedByte(in.readerIndex() + 1);
if (first == 0xfe && second == 0x01) {
in.skipBytes(in.readableBytes());
out.add(new LegacyPing());
} else if (first == 0x02) {
in.skipBytes(in.readableBytes());
out.add(new LegacyHandshake());
} else {
ctx.pipeline().remove(this);
}
}
}

View File

@@ -5,23 +5,25 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import java.nio.charset.StandardCharsets;
@ChannelHandler.Sharable
public class LegacyPingEncoder extends MessageToByteEncoder<LegacyDisconnect> {
public static final LegacyPingEncoder INSTANCE = new LegacyPingEncoder();
private LegacyPingEncoder() {}
public static final LegacyPingEncoder INSTANCE = new LegacyPingEncoder();
@Override
protected void encode(ChannelHandlerContext ctx, LegacyDisconnect msg, ByteBuf out) throws Exception {
out.writeByte(0xff);
writeLegacyString(out, msg.getReason());
}
private LegacyPingEncoder() {
}
private static void writeLegacyString(ByteBuf out, String string) {
out.writeShort(string.length());
out.writeBytes(string.getBytes(StandardCharsets.UTF_16BE));
}
@Override
protected void encode(ChannelHandlerContext ctx, LegacyDisconnect msg, ByteBuf out)
throws Exception {
out.writeByte(0xff);
writeLegacyString(out, msg.getReason());
}
private static void writeLegacyString(ByteBuf out, String string) {
out.writeShort(string.length());
out.writeBytes(string.getBytes(StandardCharsets.UTF_16BE));
}
}

View File

@@ -5,30 +5,30 @@ import com.velocitypowered.natives.encryption.VelocityCipher;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class MinecraftCipherDecoder extends ByteToMessageDecoder {
private final VelocityCipher cipher;
public MinecraftCipherDecoder(VelocityCipher cipher) {
this.cipher = Preconditions.checkNotNull(cipher, "cipher");
}
private final VelocityCipher cipher;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
ByteBuf decrypted = ctx.alloc().buffer(in.readableBytes());
try {
cipher.process(in, decrypted);
out.add(decrypted);
} catch (Exception e) {
decrypted.release();
throw e;
}
}
public MinecraftCipherDecoder(VelocityCipher cipher) {
this.cipher = Preconditions.checkNotNull(cipher, "cipher");
}
@Override
protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
cipher.dispose();
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
ByteBuf decrypted = ctx.alloc().buffer(in.readableBytes());
try {
cipher.process(in, decrypted);
out.add(decrypted);
} catch (Exception e) {
decrypted.release();
throw e;
}
}
@Override
protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
cipher.dispose();
}
}

View File

@@ -7,24 +7,26 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MinecraftCipherEncoder extends MessageToByteEncoder<ByteBuf> {
private final VelocityCipher cipher;
public MinecraftCipherEncoder(VelocityCipher cipher) {
this.cipher = Preconditions.checkNotNull(cipher, "cipher");
}
private final VelocityCipher cipher;
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
cipher.process(msg, out);
}
public MinecraftCipherEncoder(VelocityCipher cipher) {
this.cipher = Preconditions.checkNotNull(cipher, "cipher");
}
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception {
return ctx.alloc().directBuffer(msg.readableBytes());
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
cipher.process(msg, out);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
cipher.dispose();
}
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
throws Exception {
return ctx.alloc().directBuffer(msg.readableBytes());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
cipher.dispose();
}
}

View File

@@ -6,46 +6,49 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final int MAXIMUM_INITIAL_BUFFER_SIZE = 65536; // 64KiB
private final int threshold;
private final VelocityCompressor compressor;
private static final int MAXIMUM_INITIAL_BUFFER_SIZE = 65536; // 64KiB
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) {
this.threshold = threshold;
this.compressor = compressor;
private final int threshold;
private final VelocityCompressor compressor;
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) {
this.threshold = threshold;
this.compressor = compressor;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
int uncompressedSize = ProtocolUtils.readVarInt(msg);
if (uncompressedSize == 0) {
// Strip the now-useless uncompressed size, this message is already uncompressed.
out.add(msg.retainedSlice());
msg.skipBytes(msg.readableBytes());
return;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
int uncompressedSize = ProtocolUtils.readVarInt(msg);
if (uncompressedSize == 0) {
// Strip the now-useless uncompressed size, this message is already uncompressed.
out.add(msg.retainedSlice());
msg.skipBytes(msg.readableBytes());
return;
}
Preconditions.checkState(uncompressedSize >= threshold, "Uncompressed size %s doesn't make sense with threshold %s", uncompressedSize, threshold);
// Try to use the uncompressed size, but place a cap if it might be too big (possibly malicious).
ByteBuf uncompressed = ctx.alloc().buffer(Math.min(uncompressedSize, MAXIMUM_INITIAL_BUFFER_SIZE));
try {
compressor.inflate(msg, uncompressed);
Preconditions.checkState(uncompressedSize == uncompressed.readableBytes(), "Mismatched compression sizes");
out.add(uncompressed);
} catch (Exception e) {
// If something went wrong, rethrow the exception, but ensure we free our temporary buffer first.
uncompressed.release();
throw e;
}
Preconditions.checkState(uncompressedSize >= threshold,
"Uncompressed size %s doesn't make sense with threshold %s", uncompressedSize, threshold);
// Try to use the uncompressed size, but place a cap if it might be too big (possibly malicious).
ByteBuf uncompressed = ctx.alloc()
.buffer(Math.min(uncompressedSize, MAXIMUM_INITIAL_BUFFER_SIZE));
try {
compressor.inflate(msg, uncompressed);
Preconditions.checkState(uncompressedSize == uncompressed.readableBytes(),
"Mismatched compression sizes");
out.add(uncompressed);
} catch (Exception e) {
// If something went wrong, rethrow the exception, but ensure we free our temporary buffer first.
uncompressed.release();
throw e;
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
compressor.dispose();
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
compressor.dispose();
}
}

View File

@@ -7,38 +7,40 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
private final int threshold;
private final VelocityCompressor compressor;
public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) {
this.threshold = threshold;
this.compressor = compressor;
}
private final int threshold;
private final VelocityCompressor compressor;
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
int uncompressed = msg.readableBytes();
if (uncompressed <= threshold) {
// Under the threshold, there is nothing to do.
ProtocolUtils.writeVarInt(out, 0);
out.writeBytes(msg);
} else {
ProtocolUtils.writeVarInt(out, uncompressed);
compressor.deflate(msg, out);
}
}
public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) {
this.threshold = threshold;
this.compressor = compressor;
}
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception {
if (msg.readableBytes() <= threshold) {
return ctx.alloc().directBuffer(msg.readableBytes() + 1);
}
// A reasonable assumption about compression savings
return ctx.alloc().directBuffer(msg.readableBytes() / 3);
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
int uncompressed = msg.readableBytes();
if (uncompressed <= threshold) {
// Under the threshold, there is nothing to do.
ProtocolUtils.writeVarInt(out, 0);
out.writeBytes(msg);
} else {
ProtocolUtils.writeVarInt(out, uncompressed);
compressor.deflate(msg, out);
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
compressor.dispose();
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
throws Exception {
if (msg.readableBytes() <= threshold) {
return ctx.alloc().directBuffer(msg.readableBytes() + 1);
}
// A reasonable assumption about compression savings
return ctx.alloc().directBuffer(msg.readableBytes() / 3);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
compressor.dispose();
}
}

View File

@@ -9,54 +9,59 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
public class MinecraftDecoder extends MessageToMessageDecoder<ByteBuf> {
private final ProtocolConstants.Direction direction;
private StateRegistry state;
private StateRegistry.PacketRegistry.ProtocolVersion protocolVersion;
public MinecraftDecoder(ProtocolConstants.Direction direction) {
this.direction = Preconditions.checkNotNull(direction, "direction");
this.protocolVersion = direction.getProtocol(StateRegistry.HANDSHAKE, ProtocolConstants.MINIMUM_GENERIC_VERSION);
this.state = StateRegistry.HANDSHAKE;
private final ProtocolConstants.Direction direction;
private StateRegistry state;
private StateRegistry.PacketRegistry.ProtocolVersion protocolVersion;
public MinecraftDecoder(ProtocolConstants.Direction direction) {
this.direction = Preconditions.checkNotNull(direction, "direction");
this.protocolVersion = direction
.getProtocol(StateRegistry.HANDSHAKE, ProtocolConstants.MINIMUM_GENERIC_VERSION);
this.state = StateRegistry.HANDSHAKE;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
if (!msg.isReadable()) {
return;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
if (!msg.isReadable()) {
return;
}
ByteBuf slice = msg.slice();
ByteBuf slice = msg.slice();
int packetId = ProtocolUtils.readVarInt(msg);
MinecraftPacket packet = this.protocolVersion.createPacket(packetId);
if (packet == null) {
msg.skipBytes(msg.readableBytes());
out.add(slice.retain());
} else {
try {
packet.decode(msg, direction, protocolVersion.version);
} catch (Exception e) {
throw new CorruptedFrameException("Error decoding " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion.version + " State " + state + " ID " + Integer.toHexString(packetId), e);
}
if (msg.isReadable()) {
throw new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion.version + " State " + state + " ID " + Integer.toHexString(packetId));
}
out.add(packet);
}
int packetId = ProtocolUtils.readVarInt(msg);
MinecraftPacket packet = this.protocolVersion.createPacket(packetId);
if (packet == null) {
msg.skipBytes(msg.readableBytes());
out.add(slice.retain());
} else {
try {
packet.decode(msg, direction, protocolVersion.version);
} catch (Exception e) {
throw new CorruptedFrameException(
"Error decoding " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion.version + " State " + state + " ID " + Integer
.toHexString(packetId), e);
}
if (msg.isReadable()) {
throw new CorruptedFrameException(
"Did not read full packet for " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion.version + " State " + state + " ID " + Integer
.toHexString(packetId));
}
out.add(packet);
}
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = direction.getProtocol(state, protocolVersion);
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = direction.getProtocol(state, protocolVersion);
}
public void setState(StateRegistry state) {
this.state = state;
this.setProtocolVersion(protocolVersion.version);
}
public void setState(StateRegistry state) {
this.state = state;
this.setProtocolVersion(protocolVersion.version);
}
}

View File

@@ -10,29 +10,31 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MinecraftEncoder extends MessageToByteEncoder<MinecraftPacket> {
private final ProtocolConstants.Direction direction;
private StateRegistry state;
private StateRegistry.PacketRegistry.ProtocolVersion protocolVersion;
public MinecraftEncoder(ProtocolConstants.Direction direction) {
this.direction = Preconditions.checkNotNull(direction, "direction");
this.protocolVersion = direction.getProtocol(StateRegistry.HANDSHAKE, ProtocolConstants.MINIMUM_GENERIC_VERSION);
this.state = StateRegistry.HANDSHAKE;
}
private final ProtocolConstants.Direction direction;
private StateRegistry state;
private StateRegistry.PacketRegistry.ProtocolVersion protocolVersion;
@Override
protected void encode(ChannelHandlerContext ctx, MinecraftPacket msg, ByteBuf out) {
int packetId = this.protocolVersion.getPacketId(msg);
ProtocolUtils.writeVarInt(out, packetId);
msg.encode(out, direction, protocolVersion.version);
}
public MinecraftEncoder(ProtocolConstants.Direction direction) {
this.direction = Preconditions.checkNotNull(direction, "direction");
this.protocolVersion = direction
.getProtocol(StateRegistry.HANDSHAKE, ProtocolConstants.MINIMUM_GENERIC_VERSION);
this.state = StateRegistry.HANDSHAKE;
}
public void setProtocolVersion(final int protocolVersion) {
this.protocolVersion = direction.getProtocol(state, protocolVersion);
}
@Override
protected void encode(ChannelHandlerContext ctx, MinecraftPacket msg, ByteBuf out) {
int packetId = this.protocolVersion.getPacketId(msg);
ProtocolUtils.writeVarInt(out, packetId);
msg.encode(out, direction, protocolVersion.version);
}
public void setState(StateRegistry state) {
this.state = state;
this.setProtocolVersion(protocolVersion.version);
}
public void setProtocolVersion(final int protocolVersion) {
this.protocolVersion = direction.getProtocol(state, protocolVersion);
}
public void setState(StateRegistry state) {
this.state = state;
this.setProtocolVersion(protocolVersion.version);
}
}

View File

@@ -6,42 +6,42 @@ import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import java.util.List;
public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (!in.isReadable()) {
return;
}
in.markReaderIndex();
byte[] lenBuf = new byte[3];
for (int i = 0; i < lenBuf.length; i++) {
if (!in.isReadable()) {
in.resetReaderIndex();
return;
}
lenBuf[i] = in.readByte();
if (lenBuf[i] > 0) {
int packetLength = ProtocolUtils.readVarInt(Unpooled.wrappedBuffer(lenBuf));
if (packetLength == 0) {
return;
}
if (in.readableBytes() < packetLength) {
in.resetReaderIndex();
return;
}
out.add(in.readRetainedSlice(packetLength));
return;
}
}
throw new CorruptedFrameException("VarInt too big");
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (!in.isReadable()) {
return;
}
in.markReaderIndex();
byte[] lenBuf = new byte[3];
for (int i = 0; i < lenBuf.length; i++) {
if (!in.isReadable()) {
in.resetReaderIndex();
return;
}
lenBuf[i] = in.readByte();
if (lenBuf[i] > 0) {
int packetLength = ProtocolUtils.readVarInt(Unpooled.wrappedBuffer(lenBuf));
if (packetLength == 0) {
return;
}
if (in.readableBytes() < packetLength) {
in.resetReaderIndex();
return;
}
out.add(in.readRetainedSlice(packetLength));
return;
}
}
throw new CorruptedFrameException("VarInt too big");
}
}

View File

@@ -5,20 +5,22 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.util.List;
@ChannelHandler.Sharable
public class MinecraftVarintLengthEncoder extends MessageToMessageEncoder<ByteBuf> {
public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder();
private MinecraftVarintLengthEncoder() { }
public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder();
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) throws Exception {
ByteBuf lengthBuf = ctx.alloc().buffer(5); // the maximum size of a varint
ProtocolUtils.writeVarInt(lengthBuf, buf.readableBytes());
list.add(lengthBuf);
list.add(buf.retain());
}
private MinecraftVarintLengthEncoder() {
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list)
throws Exception {
ByteBuf lengthBuf = ctx.alloc().buffer(5); // the maximum size of a varint
ProtocolUtils.writeVarInt(lengthBuf, buf.readableBytes());
list.add(lengthBuf);
list.add(buf.retain());
}
}

View File

@@ -5,172 +5,172 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.UUID;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.UUID;
public class BossBar implements MinecraftPacket {
public static final int ADD = 0;
public static final int REMOVE = 1;
public static final int UPDATE_PERCENT = 2;
public static final int UPDATE_NAME = 3;
public static final int UPDATE_STYLE = 4;
public static final int UPDATE_PROPERTIES = 5;
private @Nullable UUID uuid;
private int action;
private @Nullable String name;
private float percent;
private int color;
private int overlay;
private short flags;
public UUID getUuid() {
if (uuid == null) {
throw new IllegalStateException("No boss bar UUID specified");
public static final int ADD = 0;
public static final int REMOVE = 1;
public static final int UPDATE_PERCENT = 2;
public static final int UPDATE_NAME = 3;
public static final int UPDATE_STYLE = 4;
public static final int UPDATE_PROPERTIES = 5;
private @Nullable UUID uuid;
private int action;
private @Nullable String name;
private float percent;
private int color;
private int overlay;
private short flags;
public UUID getUuid() {
if (uuid == null) {
throw new IllegalStateException("No boss bar UUID specified");
}
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public int getAction() {
return action;
}
public void setAction(int action) {
this.action = action;
}
public @Nullable String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPercent() {
return percent;
}
public void setPercent(float percent) {
this.percent = percent;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getOverlay() {
return overlay;
}
public void setOverlay(int overlay) {
this.overlay = overlay;
}
public short getFlags() {
return flags;
}
public void setFlags(short flags) {
this.flags = flags;
}
@Override
public String toString() {
return "BossBar{" +
"uuid=" + uuid +
", action=" + action +
", name='" + name + '\'' +
", percent=" + percent +
", color=" + color +
", overlay=" + overlay +
", flags=" + flags +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.uuid = ProtocolUtils.readUuid(buf);
this.action = ProtocolUtils.readVarInt(buf);
switch (action) {
case ADD:
this.name = ProtocolUtils.readString(buf);
this.percent = buf.readFloat();
this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf);
this.flags = buf.readUnsignedByte();
break;
case REMOVE:
break;
case UPDATE_PERCENT:
this.percent = buf.readFloat();
break;
case UPDATE_NAME:
this.name = ProtocolUtils.readString(buf);
break;
case UPDATE_STYLE:
this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf);
break;
case UPDATE_PROPERTIES:
this.flags = buf.readUnsignedByte();
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (uuid == null) {
throw new IllegalStateException("No boss bar UUID specified");
}
ProtocolUtils.writeUuid(buf, uuid);
ProtocolUtils.writeVarInt(buf, action);
switch (action) {
case ADD:
if (name == null) {
throw new IllegalStateException("No name specified!");
}
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public int getAction() {
return action;
}
public void setAction(int action) {
this.action = action;
}
public @Nullable String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPercent() {
return percent;
}
public void setPercent(float percent) {
this.percent = percent;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getOverlay() {
return overlay;
}
public void setOverlay(int overlay) {
this.overlay = overlay;
}
public short getFlags() {
return flags;
}
public void setFlags(short flags) {
this.flags = flags;
}
@Override
public String toString() {
return "BossBar{" +
"uuid=" + uuid +
", action=" + action +
", name='" + name + '\'' +
", percent=" + percent +
", color=" + color +
", overlay=" + overlay +
", flags=" + flags +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.uuid = ProtocolUtils.readUuid(buf);
this.action = ProtocolUtils.readVarInt(buf);
switch (action) {
case ADD:
this.name = ProtocolUtils.readString(buf);
this.percent = buf.readFloat();
this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf);
this.flags = buf.readUnsignedByte();
break;
case REMOVE:
break;
case UPDATE_PERCENT:
this.percent = buf.readFloat();
break;
case UPDATE_NAME:
this.name = ProtocolUtils.readString(buf);
break;
case UPDATE_STYLE:
this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf);
break;
case UPDATE_PROPERTIES:
this.flags = buf.readUnsignedByte();
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
ProtocolUtils.writeString(buf, name);
buf.writeFloat(percent);
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
buf.writeByte(flags);
break;
case REMOVE:
break;
case UPDATE_PERCENT:
buf.writeFloat(percent);
break;
case UPDATE_NAME:
if (name == null) {
throw new IllegalStateException("No name specified!");
}
ProtocolUtils.writeString(buf, name);
break;
case UPDATE_STYLE:
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
break;
case UPDATE_PROPERTIES:
buf.writeByte(flags);
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (uuid == null) {
throw new IllegalStateException("No boss bar UUID specified");
}
ProtocolUtils.writeUuid(buf, uuid);
ProtocolUtils.writeVarInt(buf, action);
switch (action) {
case ADD:
if (name == null) {
throw new IllegalStateException("No name specified!");
}
ProtocolUtils.writeString(buf, name);
buf.writeFloat(percent);
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
buf.writeByte(flags);
break;
case REMOVE:
break;
case UPDATE_PERCENT:
buf.writeFloat(percent);
break;
case UPDATE_NAME:
if (name == null) {
throw new IllegalStateException("No name specified!");
}
ProtocolUtils.writeString(buf, name);
break;
case UPDATE_STYLE:
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
break;
case UPDATE_PROPERTIES:
buf.writeByte(flags);
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -11,81 +11,82 @@ import net.kyori.text.serializer.ComponentSerializers;
import org.checkerframework.checker.nullness.qual.Nullable;
public class Chat implements MinecraftPacket {
public static final byte CHAT_TYPE = (byte) 0;
public static final int MAX_SERVERBOUND_MESSAGE_LENGTH = 256;
private @Nullable String message;
private byte type;
public static final byte CHAT_TYPE = (byte) 0;
public static final int MAX_SERVERBOUND_MESSAGE_LENGTH = 256;
public Chat() {
private @Nullable String message;
private byte type;
public Chat() {
}
public Chat(String message, byte type) {
this.message = message;
this.type = type;
}
public String getMessage() {
if (message == null) {
throw new IllegalStateException("Message is not specified");
}
return message;
}
public Chat(String message, byte type) {
this.message = message;
this.type = type;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
if (message == null) {
throw new IllegalStateException("Message is not specified");
}
return message;
}
public byte getType() {
return type;
}
public void setMessage(String message) {
this.message = message;
}
public void setType(byte type) {
this.type = type;
}
public byte getType() {
return type;
}
@Override
public String toString() {
return "Chat{" +
"message='" + message + '\'' +
", type=" + type +
'}';
}
public void setType(byte type) {
this.type = type;
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
message = ProtocolUtils.readString(buf);
if (direction == ProtocolConstants.Direction.CLIENTBOUND) {
type = buf.readByte();
}
}
@Override
public String toString() {
return "Chat{" +
"message='" + message + '\'' +
", type=" + type +
'}';
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (message == null) {
throw new IllegalStateException("Message is not specified");
}
ProtocolUtils.writeString(buf, message);
if (direction == ProtocolConstants.Direction.CLIENTBOUND) {
buf.writeByte(type);
}
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
message = ProtocolUtils.readString(buf);
if (direction == ProtocolConstants.Direction.CLIENTBOUND) {
type = buf.readByte();
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (message == null) {
throw new IllegalStateException("Message is not specified");
}
ProtocolUtils.writeString(buf, message);
if (direction == ProtocolConstants.Direction.CLIENTBOUND) {
buf.writeByte(type);
}
}
public static Chat createClientbound(Component component) {
return createClientbound(component, CHAT_TYPE);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
public static Chat createClientbound(Component component, byte type) {
Preconditions.checkNotNull(component, "component");
return new Chat(ComponentSerializers.JSON.serialize(component), type);
}
public static Chat createClientbound(Component component) {
return createClientbound(component, CHAT_TYPE);
}
public static Chat createClientbound(Component component, byte type) {
Preconditions.checkNotNull(component, "component");
return new Chat(ComponentSerializers.JSON.serialize(component), type);
}
public static Chat createServerbound(String message) {
return new Chat(message, CHAT_TYPE);
}
public static Chat createServerbound(String message) {
return new Chat(message, CHAT_TYPE);
}
}

View File

@@ -9,119 +9,120 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public class ClientSettings implements MinecraftPacket {
private @Nullable String locale;
private byte viewDistance;
private int chatVisibility;
private boolean chatColors;
private short skinParts;
private int mainHand;
private @Nullable String locale;
private byte viewDistance;
private int chatVisibility;
private boolean chatColors;
private short skinParts;
private int mainHand;
public ClientSettings() {
public ClientSettings() {
}
public ClientSettings(String locale, byte viewDistance, int chatVisibility, boolean chatColors,
short skinParts, int mainHand) {
this.locale = locale;
this.viewDistance = viewDistance;
this.chatVisibility = chatVisibility;
this.chatColors = chatColors;
this.skinParts = skinParts;
this.mainHand = mainHand;
}
public String getLocale() {
if (locale == null) {
throw new IllegalStateException("No locale specified");
}
return locale;
}
public ClientSettings(String locale, byte viewDistance, int chatVisibility, boolean chatColors, short skinParts, int mainHand) {
this.locale = locale;
this.viewDistance = viewDistance;
this.chatVisibility = chatVisibility;
this.chatColors = chatColors;
this.skinParts = skinParts;
this.mainHand = mainHand;
public void setLocale(String locale) {
this.locale = locale;
}
public byte getViewDistance() {
return viewDistance;
}
public void setViewDistance(byte viewDistance) {
this.viewDistance = viewDistance;
}
public int getChatVisibility() {
return chatVisibility;
}
public void setChatVisibility(int chatVisibility) {
this.chatVisibility = chatVisibility;
}
public boolean isChatColors() {
return chatColors;
}
public void setChatColors(boolean chatColors) {
this.chatColors = chatColors;
}
public short getSkinParts() {
return skinParts;
}
public void setSkinParts(short skinParts) {
this.skinParts = skinParts;
}
public int getMainHand() {
return mainHand;
}
public void setMainHand(int mainHand) {
this.mainHand = mainHand;
}
@Override
public String toString() {
return "ClientSettings{"
+ "locale='" + locale + '\''
+ ", viewDistance=" + viewDistance
+ ", chatVisibility=" + chatVisibility
+ ", chatColors=" + chatColors
+ ", skinParts=" + skinParts
+ ", mainHand=" + mainHand
+ '}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.locale = ProtocolUtils.readString(buf, 16);
this.viewDistance = buf.readByte();
this.chatVisibility = ProtocolUtils.readVarInt(buf);
this.chatColors = buf.readBoolean();
this.skinParts = buf.readUnsignedByte();
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) {
this.mainHand = ProtocolUtils.readVarInt(buf);
}
}
public String getLocale() {
if (locale == null) {
throw new IllegalStateException("No locale specified");
}
return locale;
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (locale == null) {
throw new IllegalStateException("No locale specified");
}
ProtocolUtils.writeString(buf, locale);
buf.writeByte(viewDistance);
ProtocolUtils.writeVarInt(buf, chatVisibility);
buf.writeBoolean(chatColors);
buf.writeByte(skinParts);
public void setLocale(String locale) {
this.locale = locale;
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) {
ProtocolUtils.writeVarInt(buf, mainHand);
}
}
public byte getViewDistance() {
return viewDistance;
}
public void setViewDistance(byte viewDistance) {
this.viewDistance = viewDistance;
}
public int getChatVisibility() {
return chatVisibility;
}
public void setChatVisibility(int chatVisibility) {
this.chatVisibility = chatVisibility;
}
public boolean isChatColors() {
return chatColors;
}
public void setChatColors(boolean chatColors) {
this.chatColors = chatColors;
}
public short getSkinParts() {
return skinParts;
}
public void setSkinParts(short skinParts) {
this.skinParts = skinParts;
}
public int getMainHand() {
return mainHand;
}
public void setMainHand(int mainHand) {
this.mainHand = mainHand;
}
@Override
public String toString() {
return "ClientSettings{"
+ "locale='" + locale + '\''
+ ", viewDistance=" + viewDistance
+ ", chatVisibility=" + chatVisibility
+ ", chatColors=" + chatColors
+ ", skinParts=" + skinParts
+ ", mainHand=" + mainHand
+ '}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.locale = ProtocolUtils.readString(buf, 16);
this.viewDistance = buf.readByte();
this.chatVisibility = ProtocolUtils.readVarInt(buf);
this.chatColors = buf.readBoolean();
this.skinParts = buf.readUnsignedByte();
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) {
this.mainHand = ProtocolUtils.readVarInt(buf);
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (locale == null) {
throw new IllegalStateException("No locale specified");
}
ProtocolUtils.writeString(buf, locale);
buf.writeByte(viewDistance);
ProtocolUtils.writeVarInt(buf, chatVisibility);
buf.writeBoolean(chatColors);
buf.writeByte(skinParts);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) {
ProtocolUtils.writeVarInt(buf, mainHand);
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -11,53 +11,54 @@ import net.kyori.text.serializer.ComponentSerializers;
import org.checkerframework.checker.nullness.qual.Nullable;
public class Disconnect implements MinecraftPacket {
private @Nullable String reason;
public Disconnect() {
}
private @Nullable String reason;
public Disconnect(String reason) {
this.reason = Preconditions.checkNotNull(reason, "reason");
}
public Disconnect() {
}
public String getReason() {
if (reason == null) {
throw new IllegalStateException("No reason specified");
}
return reason;
}
public Disconnect(String reason) {
this.reason = Preconditions.checkNotNull(reason, "reason");
}
public void setReason(@Nullable String reason) {
this.reason = reason;
public String getReason() {
if (reason == null) {
throw new IllegalStateException("No reason specified");
}
return reason;
}
@Override
public String toString() {
return "Disconnect{" +
"reason='" + reason + '\'' +
'}';
}
public void setReason(@Nullable String reason) {
this.reason = reason;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
reason = ProtocolUtils.readString(buf);
}
@Override
public String toString() {
return "Disconnect{" +
"reason='" + reason + '\'' +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (reason == null) {
throw new IllegalStateException("No reason specified.");
}
ProtocolUtils.writeString(buf, reason);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
reason = ProtocolUtils.readString(buf);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (reason == null) {
throw new IllegalStateException("No reason specified.");
}
ProtocolUtils.writeString(buf, reason);
}
public static Disconnect create(Component component) {
Preconditions.checkNotNull(component, "component");
return new Disconnect(ComponentSerializers.JSON.serialize(component));
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
public static Disconnect create(Component component) {
Preconditions.checkNotNull(component, "component");
return new Disconnect(ComponentSerializers.JSON.serialize(component));
}
}

View File

@@ -1,60 +1,60 @@
package com.velocitypowered.proxy.protocol.packet;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.Arrays;
import static com.velocitypowered.proxy.connection.VelocityConstants.*;
public class EncryptionRequest implements MinecraftPacket {
private String serverId = "";
private byte[] publicKey = EMPTY_BYTE_ARRAY;
private byte[] verifyToken = EMPTY_BYTE_ARRAY;
public byte[] getPublicKey() {
return publicKey;
}
private String serverId = "";
private byte[] publicKey = EMPTY_BYTE_ARRAY;
private byte[] verifyToken = EMPTY_BYTE_ARRAY;
public void setPublicKey(byte[] publicKey) {
this.publicKey = publicKey;
}
public byte[] getPublicKey() {
return publicKey;
}
public byte[] getVerifyToken() {
return verifyToken;
}
public void setPublicKey(byte[] publicKey) {
this.publicKey = publicKey;
}
public void setVerifyToken(byte[] verifyToken) {
this.verifyToken = verifyToken;
}
public byte[] getVerifyToken() {
return verifyToken;
}
@Override
public String toString() {
return "EncryptionRequest{" +
"publicKey=" + Arrays.toString(publicKey) +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
public void setVerifyToken(byte[] verifyToken) {
this.verifyToken = verifyToken;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.serverId = ProtocolUtils.readString(buf, 20);
publicKey = ProtocolUtils.readByteArray(buf, 256);
verifyToken = ProtocolUtils.readByteArray(buf, 16);
}
@Override
public String toString() {
return "EncryptionRequest{" +
"publicKey=" + Arrays.toString(publicKey) +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, this.serverId);
ProtocolUtils.writeByteArray(buf, publicKey);
ProtocolUtils.writeByteArray(buf, verifyToken);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.serverId = ProtocolUtils.readString(buf, 20);
publicKey = ProtocolUtils.readByteArray(buf, 256);
verifyToken = ProtocolUtils.readByteArray(buf, 16);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, this.serverId);
ProtocolUtils.writeByteArray(buf, publicKey);
ProtocolUtils.writeByteArray(buf, verifyToken);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -1,57 +1,57 @@
package com.velocitypowered.proxy.protocol.packet;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.Arrays;
import static com.velocitypowered.proxy.connection.VelocityConstants.*;
public class EncryptionResponse implements MinecraftPacket {
private byte[] sharedSecret = EMPTY_BYTE_ARRAY;
private byte[] verifyToken = EMPTY_BYTE_ARRAY;
public byte[] getSharedSecret() {
return sharedSecret;
}
private byte[] sharedSecret = EMPTY_BYTE_ARRAY;
private byte[] verifyToken = EMPTY_BYTE_ARRAY;
public void setSharedSecret(byte[] sharedSecret) {
this.sharedSecret = sharedSecret;
}
public byte[] getSharedSecret() {
return sharedSecret;
}
public byte[] getVerifyToken() {
return verifyToken;
}
public void setSharedSecret(byte[] sharedSecret) {
this.sharedSecret = sharedSecret;
}
public void setVerifyToken(byte[] verifyToken) {
this.verifyToken = verifyToken;
}
public byte[] getVerifyToken() {
return verifyToken;
}
@Override
public String toString() {
return "EncryptionResponse{" +
"sharedSecret=" + Arrays.toString(sharedSecret) +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
public void setVerifyToken(byte[] verifyToken) {
this.verifyToken = verifyToken;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.sharedSecret = ProtocolUtils.readByteArray(buf, 256);
this.verifyToken = ProtocolUtils.readByteArray(buf, 128);
}
@Override
public String toString() {
return "EncryptionResponse{" +
"sharedSecret=" + Arrays.toString(sharedSecret) +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeByteArray(buf, sharedSecret);
ProtocolUtils.writeByteArray(buf, verifyToken);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.sharedSecret = ProtocolUtils.readByteArray(buf, 256);
this.verifyToken = ProtocolUtils.readByteArray(buf, 128);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeByteArray(buf, sharedSecret);
ProtocolUtils.writeByteArray(buf, verifyToken);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -7,71 +7,72 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class Handshake implements MinecraftPacket {
private int protocolVersion;
private String serverAddress = "";
private int port;
private int nextStatus;
public int getProtocolVersion() {
return protocolVersion;
}
private int protocolVersion;
private String serverAddress = "";
private int port;
private int nextStatus;
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
}
public int getProtocolVersion() {
return protocolVersion;
}
public String getServerAddress() {
return serverAddress;
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
}
public void setServerAddress(String serverAddress) {
this.serverAddress = serverAddress;
}
public String getServerAddress() {
return serverAddress;
}
public int getPort() {
return port;
}
public void setServerAddress(String serverAddress) {
this.serverAddress = serverAddress;
}
public void setPort(int port) {
this.port = port;
}
public int getPort() {
return port;
}
public int getNextStatus() {
return nextStatus;
}
public void setPort(int port) {
this.port = port;
}
public void setNextStatus(int nextStatus) {
this.nextStatus = nextStatus;
}
public int getNextStatus() {
return nextStatus;
}
@Override
public String toString() {
return "Handshake{" +
"protocolVersion=" + protocolVersion +
", serverAddress='" + serverAddress + '\'' +
", port=" + port +
", nextStatus=" + nextStatus +
'}';
}
public void setNextStatus(int nextStatus) {
this.nextStatus = nextStatus;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.protocolVersion = ProtocolUtils.readVarInt(buf);
this.serverAddress = ProtocolUtils.readString(buf);
this.port = buf.readUnsignedShort();
this.nextStatus = ProtocolUtils.readVarInt(buf);
}
@Override
public String toString() {
return "Handshake{" +
"protocolVersion=" + protocolVersion +
", serverAddress='" + serverAddress + '\'' +
", port=" + port +
", nextStatus=" + nextStatus +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, this.protocolVersion);
ProtocolUtils.writeString(buf, this.serverAddress);
buf.writeShort(this.port);
ProtocolUtils.writeVarInt(buf, this.nextStatus);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.protocolVersion = ProtocolUtils.readVarInt(buf);
this.serverAddress = ProtocolUtils.readString(buf);
this.port = buf.readUnsignedShort();
this.nextStatus = ProtocolUtils.readVarInt(buf);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, this.protocolVersion);
ProtocolUtils.writeString(buf, this.serverAddress);
buf.writeShort(this.port);
ProtocolUtils.writeVarInt(buf, this.nextStatus);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -1,5 +1,7 @@
package com.velocitypowered.proxy.protocol.packet;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.writeString;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
@@ -9,54 +11,53 @@ import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializer;
import net.kyori.text.serializer.ComponentSerializers;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.writeString;
public class HeaderAndFooter implements MinecraftPacket {
private static final String EMPTY_COMPONENT = "{\"translate\":\"\"}";
private static final HeaderAndFooter RESET = new HeaderAndFooter();
private String header;
private String footer;
public HeaderAndFooter() {
this(EMPTY_COMPONENT, EMPTY_COMPONENT);
}
private static final String EMPTY_COMPONENT = "{\"translate\":\"\"}";
private static final HeaderAndFooter RESET = new HeaderAndFooter();
public HeaderAndFooter(String header, String footer) {
this.header = Preconditions.checkNotNull(header, "header");
this.footer = Preconditions.checkNotNull(footer, "footer");
}
private String header;
private String footer;
public String getHeader() {
return header;
}
public HeaderAndFooter() {
this(EMPTY_COMPONENT, EMPTY_COMPONENT);
}
public String getFooter() {
return footer;
}
public HeaderAndFooter(String header, String footer) {
this.header = Preconditions.checkNotNull(header, "header");
this.footer = Preconditions.checkNotNull(footer, "footer");
}
@Override
public void decode(ByteBuf buf, Direction direction, int protocolVersion) {
throw new UnsupportedOperationException("Decode is not implemented");
}
public String getHeader() {
return header;
}
@Override
public void encode(ByteBuf buf, Direction direction, int protocolVersion) {
writeString(buf, header);
writeString(buf, footer);
}
public String getFooter() {
return footer;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void decode(ByteBuf buf, Direction direction, int protocolVersion) {
throw new UnsupportedOperationException("Decode is not implemented");
}
public static HeaderAndFooter create(Component header, Component footer) {
ComponentSerializer<Component, Component, String> json = ComponentSerializers.JSON;
return new HeaderAndFooter(json.serialize(header), json.serialize(footer));
}
public static HeaderAndFooter reset() {
return RESET;
}
@Override
public void encode(ByteBuf buf, Direction direction, int protocolVersion) {
writeString(buf, header);
writeString(buf, footer);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
public static HeaderAndFooter create(Component header, Component footer) {
ComponentSerializer<Component, Component, String> json = ComponentSerializers.JSON;
return new HeaderAndFooter(json.serialize(header), json.serialize(footer));
}
public static HeaderAndFooter reset() {
return RESET;
}
}

View File

@@ -8,121 +8,122 @@ import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
public class JoinGame implements MinecraftPacket {
private int entityId;
private short gamemode;
private int dimension;
private short difficulty;
private short maxPlayers;
private @Nullable String levelType;
private boolean reducedDebugInfo;
public int getEntityId() {
return entityId;
}
private int entityId;
private short gamemode;
private int dimension;
private short difficulty;
private short maxPlayers;
private @Nullable String levelType;
private boolean reducedDebugInfo;
public void setEntityId(int entityId) {
this.entityId = entityId;
}
public int getEntityId() {
return entityId;
}
public short getGamemode() {
return gamemode;
}
public void setEntityId(int entityId) {
this.entityId = entityId;
}
public void setGamemode(short gamemode) {
this.gamemode = gamemode;
}
public short getGamemode() {
return gamemode;
}
public int getDimension() {
return dimension;
}
public void setGamemode(short gamemode) {
this.gamemode = gamemode;
}
public void setDimension(int dimension) {
this.dimension = dimension;
}
public int getDimension() {
return dimension;
}
public short getDifficulty() {
return difficulty;
}
public void setDimension(int dimension) {
this.dimension = dimension;
}
public void setDifficulty(short difficulty) {
this.difficulty = difficulty;
}
public short getDifficulty() {
return difficulty;
}
public short getMaxPlayers() {
return maxPlayers;
}
public void setDifficulty(short difficulty) {
this.difficulty = difficulty;
}
public void setMaxPlayers(short maxPlayers) {
this.maxPlayers = maxPlayers;
}
public short getMaxPlayers() {
return maxPlayers;
}
public String getLevelType() {
if (levelType == null) {
throw new IllegalStateException("No level type specified.");
}
return levelType;
}
public void setMaxPlayers(short maxPlayers) {
this.maxPlayers = maxPlayers;
}
public void setLevelType(String levelType) {
this.levelType = levelType;
public String getLevelType() {
if (levelType == null) {
throw new IllegalStateException("No level type specified.");
}
return levelType;
}
public boolean isReducedDebugInfo() {
return reducedDebugInfo;
}
public void setLevelType(String levelType) {
this.levelType = levelType;
}
public void setReducedDebugInfo(boolean reducedDebugInfo) {
this.reducedDebugInfo = reducedDebugInfo;
}
public boolean isReducedDebugInfo() {
return reducedDebugInfo;
}
@Override
public String toString() {
return "JoinGame{" +
"entityId=" + entityId +
", gamemode=" + gamemode +
", dimension=" + dimension +
", difficulty=" + difficulty +
", maxPlayers=" + maxPlayers +
", levelType='" + levelType + '\'' +
", reducedDebugInfo=" + reducedDebugInfo +
'}';
}
public void setReducedDebugInfo(boolean reducedDebugInfo) {
this.reducedDebugInfo = reducedDebugInfo;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.entityId = buf.readInt();
this.gamemode = buf.readUnsignedByte();
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9_1) {
this.dimension = buf.readInt();
} else {
this.dimension = buf.readByte();
}
this.difficulty = buf.readUnsignedByte();
this.maxPlayers = buf.readUnsignedByte();
this.levelType = ProtocolUtils.readString(buf, 16);
this.reducedDebugInfo = buf.readBoolean();
}
@Override
public String toString() {
return "JoinGame{" +
"entityId=" + entityId +
", gamemode=" + gamemode +
", dimension=" + dimension +
", difficulty=" + difficulty +
", maxPlayers=" + maxPlayers +
", levelType='" + levelType + '\'' +
", reducedDebugInfo=" + reducedDebugInfo +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeInt(entityId);
buf.writeByte(gamemode);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9_1) {
buf.writeInt(dimension);
} else {
buf.writeByte(dimension);
}
buf.writeByte(difficulty);
buf.writeByte(maxPlayers);
if (levelType == null) {
throw new IllegalStateException("No level type specified.");
}
ProtocolUtils.writeString(buf, levelType);
buf.writeBoolean(reducedDebugInfo);
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.entityId = buf.readInt();
this.gamemode = buf.readUnsignedByte();
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9_1) {
this.dimension = buf.readInt();
} else {
this.dimension = buf.readByte();
}
this.difficulty = buf.readUnsignedByte();
this.maxPlayers = buf.readUnsignedByte();
this.levelType = ProtocolUtils.readString(buf, 16);
this.reducedDebugInfo = buf.readBoolean();
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeInt(entityId);
buf.writeByte(gamemode);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9_1) {
buf.writeInt(dimension);
} else {
buf.writeByte(dimension);
}
buf.writeByte(difficulty);
buf.writeByte(maxPlayers);
if (levelType == null) {
throw new IllegalStateException("No level type specified.");
}
ProtocolUtils.writeString(buf, levelType);
buf.writeBoolean(reducedDebugInfo);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -1,51 +1,52 @@
package com.velocitypowered.proxy.protocol.packet;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_2;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_2;
public class KeepAlive implements MinecraftPacket {
private long randomId;
public long getRandomId() {
return randomId;
}
private long randomId;
public void setRandomId(long randomId) {
this.randomId = randomId;
}
public long getRandomId() {
return randomId;
}
@Override
public String toString() {
return "KeepAlive{" +
"randomId=" + randomId +
'}';
}
public void setRandomId(long randomId) {
this.randomId = randomId;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_12_2) {
randomId = buf.readLong();
} else {
randomId = ProtocolUtils.readVarInt(buf);
}
}
@Override
public String toString() {
return "KeepAlive{" +
"randomId=" + randomId +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_12_2) {
buf.writeLong(randomId);
} else {
ProtocolUtils.writeVarInt(buf, (int) randomId);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_12_2) {
randomId = buf.readLong();
} else {
randomId = ProtocolUtils.readVarInt(buf);
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_12_2) {
buf.writeLong(randomId);
} else {
ProtocolUtils.writeVarInt(buf, (int) randomId);
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -4,29 +4,30 @@ import net.kyori.text.TextComponent;
import net.kyori.text.serializer.ComponentSerializers;
public class LegacyDisconnect {
private final String reason;
public LegacyDisconnect(String reason) {
this.reason = reason;
}
private final String reason;
public static LegacyDisconnect fromPingResponse(LegacyPingResponse response) {
String kickMessage = String.join("\0",
"§1",
Integer.toString(response.getProtocolVersion()),
response.getServerVersion(),
response.getMotd(),
Integer.toString(response.getPlayersOnline()),
Integer.toString(response.getPlayersMax())
);
return new LegacyDisconnect(kickMessage);
}
public LegacyDisconnect(String reason) {
this.reason = reason;
}
public static LegacyDisconnect from(TextComponent component) {
return new LegacyDisconnect(ComponentSerializers.LEGACY.serialize(component));
}
public static LegacyDisconnect fromPingResponse(LegacyPingResponse response) {
String kickMessage = String.join("\0",
"§1",
Integer.toString(response.getProtocolVersion()),
response.getServerVersion(),
response.getMotd(),
Integer.toString(response.getPlayersOnline()),
Integer.toString(response.getPlayersMax())
);
return new LegacyDisconnect(kickMessage);
}
public String getReason() {
return reason;
}
public static LegacyDisconnect from(TextComponent component) {
return new LegacyDisconnect(ComponentSerializers.LEGACY.serialize(component));
}
public String getReason() {
return reason;
}
}

View File

@@ -6,18 +6,19 @@ import com.velocitypowered.proxy.protocol.ProtocolConstants;
import io.netty.buffer.ByteBuf;
public class LegacyHandshake implements MinecraftPacket {
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -6,18 +6,19 @@ import com.velocitypowered.proxy.protocol.ProtocolConstants;
import io.netty.buffer.ByteBuf;
public class LegacyPing implements MinecraftPacket {
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -5,57 +5,60 @@ import com.velocitypowered.api.proxy.server.ServerPing;
import net.kyori.text.serializer.ComponentSerializers;
public class LegacyPingResponse {
private static final ServerPing.Players FAKE_PLAYERS = new ServerPing.Players(0, 0, ImmutableList.of());
private final int protocolVersion;
private final String serverVersion;
private final String motd;
private final int playersOnline;
private final int playersMax;
public LegacyPingResponse(int protocolVersion, String serverVersion, String motd, int playersOnline, int playersMax) {
this.protocolVersion = protocolVersion;
this.serverVersion = serverVersion;
this.motd = motd;
this.playersOnline = playersOnline;
this.playersMax = playersMax;
}
private static final ServerPing.Players FAKE_PLAYERS = new ServerPing.Players(0, 0,
ImmutableList.of());
private final int protocolVersion;
private final String serverVersion;
private final String motd;
private final int playersOnline;
private final int playersMax;
public int getProtocolVersion() {
return protocolVersion;
}
public LegacyPingResponse(int protocolVersion, String serverVersion, String motd,
int playersOnline, int playersMax) {
this.protocolVersion = protocolVersion;
this.serverVersion = serverVersion;
this.motd = motd;
this.playersOnline = playersOnline;
this.playersMax = playersMax;
}
public String getServerVersion() {
return serverVersion;
}
public int getProtocolVersion() {
return protocolVersion;
}
public String getMotd() {
return motd;
}
public String getServerVersion() {
return serverVersion;
}
public int getPlayersOnline() {
return playersOnline;
}
public String getMotd() {
return motd;
}
public int getPlayersMax() {
return playersMax;
}
public int getPlayersOnline() {
return playersOnline;
}
@Override
public String toString() {
return "LegacyPingResponse{" +
"protocolVersion=" + protocolVersion +
", serverVersion='" + serverVersion + '\'' +
", motd='" + motd + '\'' +
", playersOnline=" + playersOnline +
", playersMax=" + playersMax +
'}';
}
public int getPlayersMax() {
return playersMax;
}
public static LegacyPingResponse from(ServerPing ping) {
return new LegacyPingResponse(ping.getVersion().getProtocol(),
ping.getVersion().getName(),
ComponentSerializers.LEGACY.serialize(ping.getDescription()),
ping.getPlayers().orElse(FAKE_PLAYERS).getOnline(),
ping.getPlayers().orElse(FAKE_PLAYERS).getMax());
}
@Override
public String toString() {
return "LegacyPingResponse{" +
"protocolVersion=" + protocolVersion +
", serverVersion='" + serverVersion + '\'' +
", motd='" + motd + '\'' +
", playersOnline=" + playersOnline +
", playersMax=" + playersMax +
'}';
}
public static LegacyPingResponse from(ServerPing ping) {
return new LegacyPingResponse(ping.getVersion().getProtocol(),
ping.getVersion().getName(),
ComponentSerializers.LEGACY.serialize(ping.getDescription()),
ping.getPlayers().orElse(FAKE_PLAYERS).getOnline(),
ping.getPlayers().orElse(FAKE_PLAYERS).getMax());
}
}

View File

@@ -9,67 +9,68 @@ import io.netty.buffer.Unpooled;
import org.checkerframework.checker.nullness.qual.Nullable;
public class LoginPluginMessage implements MinecraftPacket {
private int id;
private @Nullable String channel;
private ByteBuf data = Unpooled.EMPTY_BUFFER;
public LoginPluginMessage() {
private int id;
private @Nullable String channel;
private ByteBuf data = Unpooled.EMPTY_BUFFER;
public LoginPluginMessage() {
}
public LoginPluginMessage(int id, @Nullable String channel, ByteBuf data) {
this.id = id;
this.channel = channel;
this.data = data;
}
public int getId() {
return id;
}
public String getChannel() {
if (channel == null) {
throw new IllegalStateException("Channel is not specified!");
}
return channel;
}
public LoginPluginMessage(int id, @Nullable String channel, ByteBuf data) {
this.id = id;
this.channel = channel;
this.data = data;
}
public ByteBuf getData() {
return data;
}
public int getId() {
return id;
}
@Override
public String toString() {
return "LoginPluginMessage{" +
"id=" + id +
", channel='" + channel + '\'' +
", data=" + data +
'}';
}
public String getChannel() {
if (channel == null) {
throw new IllegalStateException("Channel is not specified!");
}
return channel;
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.id = ProtocolUtils.readVarInt(buf);
this.channel = ProtocolUtils.readString(buf);
if (buf.isReadable()) {
this.data = buf.readSlice(buf.readableBytes());
} else {
this.data = Unpooled.EMPTY_BUFFER;
}
}
public ByteBuf getData() {
return data;
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, id);
if (channel == null) {
throw new IllegalStateException("Channel is not specified!");
}
ProtocolUtils.writeString(buf, channel);
buf.writeBytes(data);
}
@Override
public String toString() {
return "LoginPluginMessage{" +
"id=" + id +
", channel='" + channel + '\'' +
", data=" + data +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.id = ProtocolUtils.readVarInt(buf);
this.channel = ProtocolUtils.readString(buf);
if (buf.isReadable()) {
this.data = buf.readSlice(buf.readableBytes());
} else {
this.data = Unpooled.EMPTY_BUFFER;
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, id);
if (channel == null) {
throw new IllegalStateException("Channel is not specified!");
}
ProtocolUtils.writeString(buf, channel);
buf.writeBytes(data);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -8,63 +8,64 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class LoginPluginResponse implements MinecraftPacket {
private int id;
private boolean success;
private ByteBuf data = Unpooled.EMPTY_BUFFER;
public int getId() {
return id;
}
private int id;
private boolean success;
private ByteBuf data = Unpooled.EMPTY_BUFFER;
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public boolean isSuccess() {
return success;
}
public void setId(int id) {
this.id = id;
}
public void setSuccess(boolean success) {
this.success = success;
}
public boolean isSuccess() {
return success;
}
public ByteBuf getData() {
return data;
}
public void setSuccess(boolean success) {
this.success = success;
}
public void setData(ByteBuf data) {
this.data = data;
}
public ByteBuf getData() {
return data;
}
@Override
public String toString() {
return "LoginPluginResponse{" +
"id=" + id +
", success=" + success +
", data=" + data +
'}';
}
public void setData(ByteBuf data) {
this.data = data;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.id = ProtocolUtils.readVarInt(buf);
this.success = buf.readBoolean();
if (buf.isReadable()) {
this.data = buf.readSlice(buf.readableBytes());
} else {
this.data = Unpooled.EMPTY_BUFFER;
}
}
@Override
public String toString() {
return "LoginPluginResponse{" +
"id=" + id +
", success=" + success +
", data=" + data +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, id);
buf.writeBoolean(success);
buf.writeBytes(data);
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.id = ProtocolUtils.readVarInt(buf);
this.success = buf.readBoolean();
if (buf.isReadable()) {
this.data = buf.readSlice(buf.readableBytes());
} else {
this.data = Unpooled.EMPTY_BUFFER;
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, id);
buf.writeBoolean(success);
buf.writeBytes(data);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -8,194 +8,196 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class PlayerListItem implements MinecraftPacket {
public static final int ADD_PLAYER = 0;
public static final int UPDATE_GAMEMODE = 1;
public static final int UPDATE_LATENCY = 2;
public static final int UPDATE_DISPLAY_NAME = 3;
public static final int REMOVE_PLAYER = 4;
private int action;
private final List<Item> items = new ArrayList<>();
public PlayerListItem(int action, List<Item> items) {
this.action = action;
this.items.addAll(items);
public static final int ADD_PLAYER = 0;
public static final int UPDATE_GAMEMODE = 1;
public static final int UPDATE_LATENCY = 2;
public static final int UPDATE_DISPLAY_NAME = 3;
public static final int REMOVE_PLAYER = 4;
private int action;
private final List<Item> items = new ArrayList<>();
public PlayerListItem(int action, List<Item> items) {
this.action = action;
this.items.addAll(items);
}
public PlayerListItem() {
}
public int getAction() {
return action;
}
public List<Item> getItems() {
return items;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
action = ProtocolUtils.readVarInt(buf);
int length = ProtocolUtils.readVarInt(buf);
for (int i = 0; i < length; i++) {
Item item = new Item(ProtocolUtils.readUuid(buf));
items.add(item);
switch (action) {
case ADD_PLAYER: {
item.setName(ProtocolUtils.readString(buf));
item.setProperties(ProtocolUtils.readProperties(buf));
item.setGameMode(ProtocolUtils.readVarInt(buf));
item.setLatency(ProtocolUtils.readVarInt(buf));
item.setDisplayName(readOptionalComponent(buf));
break;
}
case UPDATE_GAMEMODE:
item.setGameMode(ProtocolUtils.readVarInt(buf));
break;
case UPDATE_LATENCY:
item.setLatency(ProtocolUtils.readVarInt(buf));
break;
case UPDATE_DISPLAY_NAME:
item.setDisplayName(readOptionalComponent(buf));
break;
case REMOVE_PLAYER:
//Do nothing, all that is needed is the uuid
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
}
@Nullable
private static Component readOptionalComponent(ByteBuf buf) {
if (buf.readBoolean()) {
return ComponentSerializers.JSON.deserialize(ProtocolUtils.readString(buf));
}
return null;
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, action);
ProtocolUtils.writeVarInt(buf, items.size());
for (Item item : items) {
ProtocolUtils.writeUuid(buf, item.getUuid());
switch (action) {
case ADD_PLAYER:
ProtocolUtils.writeString(buf, item.getName());
ProtocolUtils.writeProperties(buf, item.getProperties());
ProtocolUtils.writeVarInt(buf, item.getGameMode());
ProtocolUtils.writeVarInt(buf, item.getLatency());
writeDisplayName(buf, item.getDisplayName());
break;
case UPDATE_GAMEMODE:
ProtocolUtils.writeVarInt(buf, item.getGameMode());
break;
case UPDATE_LATENCY:
ProtocolUtils.writeVarInt(buf, item.getLatency());
break;
case UPDATE_DISPLAY_NAME:
writeDisplayName(buf, item.getDisplayName());
break;
case REMOVE_PLAYER:
//Do nothing, all that is needed is the uuid
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
private void writeDisplayName(ByteBuf buf, @Nullable Component displayName) {
buf.writeBoolean(displayName != null);
if (displayName != null) {
ProtocolUtils.writeString(buf, ComponentSerializers.JSON.serialize(displayName));
}
}
public static class Item {
private final UUID uuid;
private String name = "";
private List<GameProfile.Property> properties = ImmutableList.of();
private int gameMode;
private int latency;
private @Nullable Component displayName;
public Item(UUID uuid) {
this.uuid = uuid;
}
public PlayerListItem() {}
public int getAction() {
return action;
public static Item from(TabListEntry entry) {
return new Item(entry.getProfile().idAsUuid())
.setName(entry.getProfile().getName())
.setProperties(entry.getProfile().getProperties())
.setLatency(entry.getLatency())
.setGameMode(entry.getGameMode())
.setDisplayName(entry.getDisplayName().orElse(null));
}
public List<Item> getItems() {
return items;
public UUID getUuid() {
return uuid;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
action = ProtocolUtils.readVarInt(buf);
int length = ProtocolUtils.readVarInt(buf);
for (int i = 0; i < length; i++) {
Item item = new Item(ProtocolUtils.readUuid(buf));
items.add(item);
switch (action) {
case ADD_PLAYER: {
item.setName(ProtocolUtils.readString(buf));
item.setProperties(ProtocolUtils.readProperties(buf));
item.setGameMode(ProtocolUtils.readVarInt(buf));
item.setLatency(ProtocolUtils.readVarInt(buf));
item.setDisplayName(readOptionalComponent(buf));
break;
}
case UPDATE_GAMEMODE:
item.setGameMode(ProtocolUtils.readVarInt(buf));
break;
case UPDATE_LATENCY:
item.setLatency(ProtocolUtils.readVarInt(buf));
break;
case UPDATE_DISPLAY_NAME:
item.setDisplayName(readOptionalComponent(buf));
break;
case REMOVE_PLAYER:
//Do nothing, all that is needed is the uuid
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
public String getName() {
return name;
}
@Nullable
private static Component readOptionalComponent(ByteBuf buf) {
if (buf.readBoolean()) {
return ComponentSerializers.JSON.deserialize(ProtocolUtils.readString(buf));
}
return null;
public Item setName(String name) {
this.name = name;
return this;
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, action);
ProtocolUtils.writeVarInt(buf, items.size());
for (Item item : items) {
ProtocolUtils.writeUuid(buf, item.getUuid());
switch (action) {
case ADD_PLAYER:
ProtocolUtils.writeString(buf, item.getName());
ProtocolUtils.writeProperties(buf, item.getProperties());
ProtocolUtils.writeVarInt(buf, item.getGameMode());
ProtocolUtils.writeVarInt(buf, item.getLatency());
writeDisplayName(buf, item.getDisplayName());
break;
case UPDATE_GAMEMODE:
ProtocolUtils.writeVarInt(buf, item.getGameMode());
break;
case UPDATE_LATENCY:
ProtocolUtils.writeVarInt(buf, item.getLatency());
break;
case UPDATE_DISPLAY_NAME:
writeDisplayName(buf, item.getDisplayName());
break;
case REMOVE_PLAYER:
//Do nothing, all that is needed is the uuid
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
public List<GameProfile.Property> getProperties() {
return properties;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
public Item setProperties(List<GameProfile.Property> properties) {
this.properties = properties;
return this;
}
private void writeDisplayName(ByteBuf buf, @Nullable Component displayName) {
buf.writeBoolean(displayName != null);
if (displayName != null) {
ProtocolUtils.writeString(buf, ComponentSerializers.JSON.serialize(displayName));
}
public int getGameMode() {
return gameMode;
}
public static class Item {
private final UUID uuid;
private String name = "";
private List<GameProfile.Property> properties = ImmutableList.of();
private int gameMode;
private int latency;
private @Nullable Component displayName;
public Item(UUID uuid) {
this.uuid = uuid;
}
public static Item from(TabListEntry entry) {
return new Item(entry.getProfile().idAsUuid())
.setName(entry.getProfile().getName())
.setProperties(entry.getProfile().getProperties())
.setLatency(entry.getLatency())
.setGameMode(entry.getGameMode())
.setDisplayName(entry.getDisplayName().orElse(null));
}
public UUID getUuid() {
return uuid;
}
public String getName() {
return name;
}
public Item setName(String name) {
this.name = name;
return this;
}
public List<GameProfile.Property> getProperties() {
return properties;
}
public Item setProperties(List<GameProfile.Property> properties) {
this.properties = properties;
return this;
}
public int getGameMode() {
return gameMode;
}
public Item setGameMode(int gamemode) {
this.gameMode = gamemode;
return this;
}
public int getLatency() {
return latency;
}
public Item setLatency(int latency) {
this.latency = latency;
return this;
}
public @Nullable Component getDisplayName() {
return displayName;
}
public Item setDisplayName(@Nullable Component displayName) {
this.displayName = displayName;
return this;
}
public Item setGameMode(int gamemode) {
this.gameMode = gamemode;
return this;
}
public int getLatency() {
return latency;
}
public Item setLatency(int latency) {
this.latency = latency;
return this;
}
public @Nullable Component getDisplayName() {
return displayName;
}
public Item setDisplayName(@Nullable Component displayName) {
this.displayName = displayName;
return this;
}
}
}

View File

@@ -1,5 +1,7 @@
package com.velocitypowered.proxy.protocol.packet;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
@@ -8,57 +10,56 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import org.checkerframework.checker.nullness.qual.Nullable;
import static com.velocitypowered.proxy.connection.VelocityConstants.*;
public class PluginMessage implements MinecraftPacket {
private @Nullable String channel;
private byte[] data = EMPTY_BYTE_ARRAY;
public String getChannel() {
if (channel == null) {
throw new IllegalStateException("Channel is not specified.");
}
return channel;
}
private @Nullable String channel;
private byte[] data = EMPTY_BYTE_ARRAY;
public void setChannel(String channel) {
this.channel = channel;
public String getChannel() {
if (channel == null) {
throw new IllegalStateException("Channel is not specified.");
}
return channel;
}
public byte[] getData() {
return data;
}
public void setChannel(String channel) {
this.channel = channel;
}
public void setData(byte[] data) {
this.data = data;
}
public byte[] getData() {
return data;
}
@Override
public String toString() {
return "PluginMessage{" +
"channel='" + channel + '\'' +
", data=" + ByteBufUtil.hexDump(data) +
'}';
}
public void setData(byte[] data) {
this.data = data;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.channel = ProtocolUtils.readString(buf);
this.data = new byte[buf.readableBytes()];
buf.readBytes(data);
}
@Override
public String toString() {
return "PluginMessage{" +
"channel='" + channel + '\'' +
", data=" + ByteBufUtil.hexDump(data) +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (channel == null) {
throw new IllegalStateException("Channel is not specified.");
}
ProtocolUtils.writeString(buf, channel);
buf.writeBytes(data);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.channel = ProtocolUtils.readString(buf);
this.data = new byte[buf.readableBytes()];
buf.readBytes(data);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (channel == null) {
throw new IllegalStateException("Channel is not specified.");
}
ProtocolUtils.writeString(buf, channel);
buf.writeBytes(data);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -7,81 +7,82 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class Respawn implements MinecraftPacket {
private int dimension;
private short difficulty;
private short gamemode;
private String levelType = "";
public Respawn() {
}
private int dimension;
private short difficulty;
private short gamemode;
private String levelType = "";
public Respawn(int dimension, short difficulty, short gamemode, String levelType) {
this.dimension = dimension;
this.difficulty = difficulty;
this.gamemode = gamemode;
this.levelType = levelType;
}
public Respawn() {
}
public int getDimension() {
return dimension;
}
public Respawn(int dimension, short difficulty, short gamemode, String levelType) {
this.dimension = dimension;
this.difficulty = difficulty;
this.gamemode = gamemode;
this.levelType = levelType;
}
public void setDimension(int dimension) {
this.dimension = dimension;
}
public int getDimension() {
return dimension;
}
public short getDifficulty() {
return difficulty;
}
public void setDimension(int dimension) {
this.dimension = dimension;
}
public void setDifficulty(short difficulty) {
this.difficulty = difficulty;
}
public short getDifficulty() {
return difficulty;
}
public short getGamemode() {
return gamemode;
}
public void setDifficulty(short difficulty) {
this.difficulty = difficulty;
}
public void setGamemode(short gamemode) {
this.gamemode = gamemode;
}
public short getGamemode() {
return gamemode;
}
public String getLevelType() {
return levelType;
}
public void setGamemode(short gamemode) {
this.gamemode = gamemode;
}
public void setLevelType(String levelType) {
this.levelType = levelType;
}
public String getLevelType() {
return levelType;
}
@Override
public String toString() {
return "Respawn{" +
"dimension=" + dimension +
", difficulty=" + difficulty +
", gamemode=" + gamemode +
", levelType='" + levelType + '\'' +
'}';
}
public void setLevelType(String levelType) {
this.levelType = levelType;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.dimension = buf.readInt();
this.difficulty = buf.readUnsignedByte();
this.gamemode = buf.readUnsignedByte();
this.levelType = ProtocolUtils.readString(buf, 16);
}
@Override
public String toString() {
return "Respawn{" +
"dimension=" + dimension +
", difficulty=" + difficulty +
", gamemode=" + gamemode +
", levelType='" + levelType + '\'' +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeInt(dimension);
buf.writeByte(difficulty);
buf.writeByte(gamemode);
ProtocolUtils.writeString(buf, levelType);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.dimension = buf.readInt();
this.difficulty = buf.readUnsignedByte();
this.gamemode = buf.readUnsignedByte();
this.levelType = ProtocolUtils.readString(buf, 16);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeInt(dimension);
buf.writeByte(difficulty);
buf.writeByte(gamemode);
ProtocolUtils.writeString(buf, levelType);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -9,43 +9,45 @@ import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
public class ServerLogin implements MinecraftPacket {
private @Nullable String username;
public ServerLogin() {}
private @Nullable String username;
public ServerLogin(String username) {
this.username = Preconditions.checkNotNull(username, "username");
public ServerLogin() {
}
public ServerLogin(String username) {
this.username = Preconditions.checkNotNull(username, "username");
}
public String getUsername() {
if (username == null) {
throw new IllegalStateException("No username found!");
}
return username;
}
public String getUsername() {
if (username == null) {
throw new IllegalStateException("No username found!");
}
return username;
}
@Override
public String toString() {
return "ServerLogin{" +
"username='" + username + '\'' +
'}';
}
@Override
public String toString() {
return "ServerLogin{" +
"username='" + username + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
username = ProtocolUtils.readString(buf, 16);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
username = ProtocolUtils.readString(buf, 16);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (username == null) {
throw new IllegalStateException("No username found!");
}
ProtocolUtils.writeString(buf, username);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (username == null) {
throw new IllegalStateException("No username found!");
}
ProtocolUtils.writeString(buf, username);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -5,64 +5,64 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.UUID;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.UUID;
public class ServerLoginSuccess implements MinecraftPacket {
private @Nullable UUID uuid;
private @Nullable String username;
public UUID getUuid() {
if (uuid == null) {
throw new IllegalStateException("No UUID specified!");
}
return uuid;
}
private @Nullable UUID uuid;
private @Nullable String username;
public void setUuid(UUID uuid) {
this.uuid = uuid;
public UUID getUuid() {
if (uuid == null) {
throw new IllegalStateException("No UUID specified!");
}
return uuid;
}
public String getUsername() {
if (username == null) {
throw new IllegalStateException("No username specified!");
}
return username;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public void setUsername(String username) {
this.username = username;
public String getUsername() {
if (username == null) {
throw new IllegalStateException("No username specified!");
}
return username;
}
@Override
public String toString() {
return "ServerLoginSuccess{" +
"uuid=" + uuid +
", username='" + username + '\'' +
'}';
}
public void setUsername(String username) {
this.username = username;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
uuid = UUID.fromString(ProtocolUtils.readString(buf, 36));
username = ProtocolUtils.readString(buf, 16);
}
@Override
public String toString() {
return "ServerLoginSuccess{" +
"uuid=" + uuid +
", username='" + username + '\'' +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (uuid == null) {
throw new IllegalStateException("No UUID specified!");
}
ProtocolUtils.writeString(buf, uuid.toString());
if (username == null) {
throw new IllegalStateException("No username specified!");
}
ProtocolUtils.writeString(buf, username);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
uuid = UUID.fromString(ProtocolUtils.readString(buf, 36));
username = ProtocolUtils.readString(buf, 16);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (uuid == null) {
throw new IllegalStateException("No UUID specified!");
}
ProtocolUtils.writeString(buf, uuid.toString());
if (username == null) {
throw new IllegalStateException("No username specified!");
}
ProtocolUtils.writeString(buf, username);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -7,41 +7,43 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class SetCompression implements MinecraftPacket {
private int threshold;
public SetCompression() {}
private int threshold;
public SetCompression(int threshold) {
this.threshold = threshold;
}
public SetCompression() {
}
public int getThreshold() {
return threshold;
}
public SetCompression(int threshold) {
this.threshold = threshold;
}
public void setThreshold(int threshold) {
this.threshold = threshold;
}
public int getThreshold() {
return threshold;
}
@Override
public String toString() {
return "SetCompression{" +
"threshold=" + threshold +
'}';
}
public void setThreshold(int threshold) {
this.threshold = threshold;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.threshold = ProtocolUtils.readVarInt(buf);
}
@Override
public String toString() {
return "SetCompression{" +
"threshold=" + threshold +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, threshold);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.threshold = ProtocolUtils.readVarInt(buf);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, threshold);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -6,20 +6,21 @@ import com.velocitypowered.proxy.protocol.ProtocolConstants;
import io.netty.buffer.ByteBuf;
public class StatusPing implements MinecraftPacket {
private long randomId;
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
randomId = buf.readLong();
}
private long randomId;
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeLong(randomId);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
randomId = buf.readLong();
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeLong(randomId);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -6,29 +6,30 @@ import com.velocitypowered.proxy.protocol.ProtocolConstants;
import io.netty.buffer.ByteBuf;
public class StatusRequest implements MinecraftPacket {
public static final StatusRequest INSTANCE = new StatusRequest();
private StatusRequest() {
public static final StatusRequest INSTANCE = new StatusRequest();
}
private StatusRequest() {
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
// There is no additional data to decode.
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
// There is no data to decode.
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
// There is no additional data to decode.
}
@Override
public String toString() {
return "StatusRequest";
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
// There is no data to decode.
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public String toString() {
return "StatusRequest";
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -8,43 +8,45 @@ import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
public class StatusResponse implements MinecraftPacket {
private @Nullable String status;
public StatusResponse() {}
private @Nullable String status;
public StatusResponse(String status) {
this.status = status;
public StatusResponse() {
}
public StatusResponse(String status) {
this.status = status;
}
public String getStatus() {
if (status == null) {
throw new IllegalStateException("Status is not specified");
}
return status;
}
public String getStatus() {
if (status == null) {
throw new IllegalStateException("Status is not specified");
}
return status;
}
@Override
public String toString() {
return "StatusResponse{" +
"status='" + status + '\'' +
'}';
}
@Override
public String toString() {
return "StatusResponse{" +
"status='" + status + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
status = ProtocolUtils.readString(buf, Short.MAX_VALUE);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
status = ProtocolUtils.readString(buf, Short.MAX_VALUE);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (status == null) {
throw new IllegalStateException("Status is not specified");
}
ProtocolUtils.writeString(buf, status);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (status == null) {
throw new IllegalStateException("Status is not specified");
}
ProtocolUtils.writeString(buf, status);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -1,5 +1,7 @@
package com.velocitypowered.proxy.protocol.packet;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_9;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
@@ -7,88 +9,87 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_9;
public class TabCompleteRequest implements MinecraftPacket {
private @Nullable String command;
private boolean assumeCommand;
private boolean hasPosition;
private long position;
public String getCommand() {
if (command == null) {
throw new IllegalStateException("Command is not specified");
}
return command;
}
private @Nullable String command;
private boolean assumeCommand;
private boolean hasPosition;
private long position;
public void setCommand(String command) {
this.command = command;
public String getCommand() {
if (command == null) {
throw new IllegalStateException("Command is not specified");
}
return command;
}
public boolean isAssumeCommand() {
return assumeCommand;
}
public void setCommand(String command) {
this.command = command;
}
public void setAssumeCommand(boolean assumeCommand) {
this.assumeCommand = assumeCommand;
}
public boolean isAssumeCommand() {
return assumeCommand;
}
public boolean isHasPosition() {
return hasPosition;
}
public void setAssumeCommand(boolean assumeCommand) {
this.assumeCommand = assumeCommand;
}
public void setHasPosition(boolean hasPosition) {
this.hasPosition = hasPosition;
}
public boolean isHasPosition() {
return hasPosition;
}
public long getPosition() {
return position;
}
public void setHasPosition(boolean hasPosition) {
this.hasPosition = hasPosition;
}
public void setPosition(long position) {
this.position = position;
}
public long getPosition() {
return position;
}
@Override
public String toString() {
return "TabCompleteRequest{" +
"command='" + command + '\'' +
", assumeCommand=" + assumeCommand +
", hasPosition=" + hasPosition +
", position=" + position +
'}';
}
public void setPosition(long position) {
this.position = position;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.command = ProtocolUtils.readString(buf);
if (protocolVersion >= MINECRAFT_1_9) {
this.assumeCommand = buf.readBoolean();
}
this.hasPosition = buf.readBoolean();
if (hasPosition) {
this.position = buf.readLong();
}
}
@Override
public String toString() {
return "TabCompleteRequest{" +
"command='" + command + '\'' +
", assumeCommand=" + assumeCommand +
", hasPosition=" + hasPosition +
", position=" + position +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (command == null) {
throw new IllegalStateException("Command is not specified");
}
ProtocolUtils.writeString(buf, command);
if (protocolVersion >= MINECRAFT_1_9) {
buf.writeBoolean(assumeCommand);
}
buf.writeBoolean(hasPosition);
if (hasPosition) {
buf.writeLong(position);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.command = ProtocolUtils.readString(buf);
if (protocolVersion >= MINECRAFT_1_9) {
this.assumeCommand = buf.readBoolean();
}
this.hasPosition = buf.readBoolean();
if (hasPosition) {
this.position = buf.readLong();
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (command == null) {
throw new IllegalStateException("Command is not specified");
}
ProtocolUtils.writeString(buf, command);
if (protocolVersion >= MINECRAFT_1_9) {
buf.writeBoolean(assumeCommand);
}
buf.writeBoolean(hasPosition);
if (hasPosition) {
buf.writeLong(position);
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -5,42 +5,42 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.ArrayList;
import java.util.List;
public class TabCompleteResponse implements MinecraftPacket {
private final List<String> offers = new ArrayList<>();
public List<String> getOffers() {
return offers;
}
private final List<String> offers = new ArrayList<>();
@Override
public String toString() {
return "TabCompleteResponse{" +
"offers=" + offers +
'}';
}
public List<String> getOffers() {
return offers;
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
int offersAvailable = ProtocolUtils.readVarInt(buf);
for (int i = 0; i < offersAvailable; i++) {
offers.add(ProtocolUtils.readString(buf));
}
}
@Override
public String toString() {
return "TabCompleteResponse{" +
"offers=" + offers +
'}';
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, offers.size());
for (String offer : offers) {
ProtocolUtils.writeString(buf, offer);
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
int offersAvailable = ProtocolUtils.readVarInt(buf);
for (int i = 0; i < offersAvailable; i++) {
offers.add(ProtocolUtils.readString(buf));
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, offers.size());
for (String offer : offers) {
ProtocolUtils.writeString(buf, offer);
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -8,146 +8,150 @@ import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
public class TitlePacket implements MinecraftPacket {
public static final int SET_TITLE = 0;
public static final int SET_SUBTITLE = 1;
public static final int SET_ACTION_BAR = 2;
public static final int SET_TIMES = 3;
public static final int SET_TIMES_OLD = 2;
public static final int HIDE = 4;
public static final int HIDE_OLD = 3;
public static final int RESET = 5;
public static final int RESET_OLD = 4;
private int action;
private @Nullable String component;
private int fadeIn;
private int stay;
private int fadeOut;
public static final int SET_TITLE = 0;
public static final int SET_SUBTITLE = 1;
public static final int SET_ACTION_BAR = 2;
public static final int SET_TIMES = 3;
public static final int SET_TIMES_OLD = 2;
public static final int HIDE = 4;
public static final int HIDE_OLD = 3;
public static final int RESET = 5;
public static final int RESET_OLD = 4;
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException(); // encode only
private int action;
private @Nullable String component;
private int fadeIn;
private int stay;
private int fadeOut;
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
throw new UnsupportedOperationException(); // encode only
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, action);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_11) {
// 1.11+ shifted the action enum by 1 to handle the action bar
switch (action) {
case SET_TITLE:
case SET_SUBTITLE:
case SET_ACTION_BAR:
if (component == null) {
throw new IllegalStateException("No component found for " + action);
}
ProtocolUtils.writeString(buf, component);
break;
case SET_TIMES:
buf.writeInt(fadeIn);
buf.writeInt(stay);
buf.writeInt(fadeOut);
break;
case HIDE:
case RESET:
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
} else {
switch (action) {
case SET_TITLE:
case SET_SUBTITLE:
if (component == null) {
throw new IllegalStateException("No component found for " + action);
}
ProtocolUtils.writeString(buf, component);
break;
case SET_TIMES_OLD:
buf.writeInt(fadeIn);
buf.writeInt(stay);
buf.writeInt(fadeOut);
break;
case HIDE_OLD:
case RESET_OLD:
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, action);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_11) {
// 1.11+ shifted the action enum by 1 to handle the action bar
switch (action) {
case SET_TITLE:
case SET_SUBTITLE:
case SET_ACTION_BAR:
if (component == null) {
throw new IllegalStateException("No component found for " + action);
}
ProtocolUtils.writeString(buf, component);
break;
case SET_TIMES:
buf.writeInt(fadeIn);
buf.writeInt(stay);
buf.writeInt(fadeOut);
break;
case HIDE:
case RESET:
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
} else {
switch (action) {
case SET_TITLE:
case SET_SUBTITLE:
if (component == null) {
throw new IllegalStateException("No component found for " + action);
}
ProtocolUtils.writeString(buf, component);
break;
case SET_TIMES_OLD:
buf.writeInt(fadeIn);
buf.writeInt(stay);
buf.writeInt(fadeOut);
break;
case HIDE_OLD:
case RESET_OLD:
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
}
}
public int getAction() {
return action;
}
public int getAction() {
return action;
}
public void setAction(int action) {
this.action = action;
}
public void setAction(int action) {
this.action = action;
}
public @Nullable String getComponent() {
return component;
}
public @Nullable String getComponent() {
return component;
}
public void setComponent(@Nullable String component) {
this.component = component;
}
public void setComponent(@Nullable String component) {
this.component = component;
}
public int getFadeIn() {
return fadeIn;
}
public int getFadeIn() {
return fadeIn;
}
public void setFadeIn(int fadeIn) {
this.fadeIn = fadeIn;
}
public void setFadeIn(int fadeIn) {
this.fadeIn = fadeIn;
}
public int getStay() {
return stay;
}
public int getStay() {
return stay;
}
public void setStay(int stay) {
this.stay = stay;
}
public void setStay(int stay) {
this.stay = stay;
}
public int getFadeOut() {
return fadeOut;
}
public int getFadeOut() {
return fadeOut;
}
public void setFadeOut(int fadeOut) {
this.fadeOut = fadeOut;
}
public void setFadeOut(int fadeOut) {
this.fadeOut = fadeOut;
}
public static TitlePacket hideForProtocolVersion(int protocolVersion) {
TitlePacket packet = new TitlePacket();
packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.HIDE
: TitlePacket.HIDE_OLD);
return packet;
}
public static TitlePacket hideForProtocolVersion(int protocolVersion) {
TitlePacket packet = new TitlePacket();
packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.HIDE : TitlePacket.HIDE_OLD);
return packet;
}
public static TitlePacket resetForProtocolVersion(int protocolVersion) {
TitlePacket packet = new TitlePacket();
packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.RESET
: TitlePacket.RESET_OLD);
return packet;
}
public static TitlePacket resetForProtocolVersion(int protocolVersion) {
TitlePacket packet = new TitlePacket();
packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.RESET : TitlePacket.RESET_OLD);
return packet;
}
public static TitlePacket timesForProtocolVersion(int protocolVersion) {
TitlePacket packet = new TitlePacket();
packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.SET_TIMES
: TitlePacket.SET_TIMES_OLD);
return packet;
}
public static TitlePacket timesForProtocolVersion(int protocolVersion) {
TitlePacket packet = new TitlePacket();
packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.SET_TIMES : TitlePacket.SET_TIMES_OLD);
return packet;
}
@Override
public String toString() {
return "TitlePacket{" +
"action=" + action +
", component='" + component + '\'' +
", fadeIn=" + fadeIn +
", stay=" + stay +
", fadeOut=" + fadeOut +
'}';
}
@Override
public String toString() {
return "TitlePacket{" +
"action=" + action +
", component='" + component + '\'' +
", fadeIn=" + fadeIn +
", stay=" + stay +
", fadeOut=" + fadeOut +
'}';
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}

View File

@@ -1,18 +1,23 @@
package com.velocitypowered.proxy.protocol.util;
import com.google.gson.*;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.velocitypowered.api.util.Favicon;
import java.lang.reflect.Type;
public class FaviconSerializer implements JsonSerializer<Favicon>, JsonDeserializer<Favicon> {
@Override
public Favicon deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
return new Favicon(json.getAsString());
}
@Override
public JsonElement serialize(Favicon src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getBase64Url());
}
@Override
public Favicon deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
return new Favicon(json.getAsString());
}
@Override
public JsonElement serialize(Favicon src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getBase64Url());
}
}

View File

@@ -7,74 +7,80 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
public class PluginMessageUtil {
public static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
public static final String BRAND_CHANNEL = "minecraft:brand";
public static final String REGISTER_CHANNEL_LEGACY = "REGISTER";
public static final String REGISTER_CHANNEL = "minecraft:register";
public static final String UNREGISTER_CHANNEL_LEGACY = "UNREGISTER";
public static final String UNREGISTER_CHANNEL = "minecraft:unregister";
private PluginMessageUtil() {
throw new AssertionError();
public static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
public static final String BRAND_CHANNEL = "minecraft:brand";
public static final String REGISTER_CHANNEL_LEGACY = "REGISTER";
public static final String REGISTER_CHANNEL = "minecraft:register";
public static final String UNREGISTER_CHANNEL_LEGACY = "UNREGISTER";
public static final String UNREGISTER_CHANNEL = "minecraft:unregister";
private PluginMessageUtil() {
throw new AssertionError();
}
public static boolean isMCBrand(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals(BRAND_CHANNEL_LEGACY) || message.getChannel()
.equals(BRAND_CHANNEL);
}
public static boolean isMCRegister(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals(REGISTER_CHANNEL_LEGACY) || message.getChannel()
.equals(REGISTER_CHANNEL);
}
public static boolean isMCUnregister(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals(UNREGISTER_CHANNEL_LEGACY) || message.getChannel()
.equals(UNREGISTER_CHANNEL);
}
public static List<String> getChannels(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
Preconditions
.checkArgument(isMCRegister(message) || isMCUnregister(message), "Unknown channel type %s",
message.getChannel());
String channels = new String(message.getData(), StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0"));
}
public static PluginMessage constructChannelsPacket(int protocolVersion,
Collection<String> channels) {
Preconditions.checkNotNull(channels, "channels");
String channelName = protocolVersion >= ProtocolConstants.MINECRAFT_1_13 ? REGISTER_CHANNEL
: REGISTER_CHANNEL_LEGACY;
PluginMessage message = new PluginMessage();
message.setChannel(channelName);
message.setData(String.join("\0", channels).getBytes(StandardCharsets.UTF_8));
return message;
}
public static PluginMessage rewriteMCBrand(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkArgument(isMCBrand(message), "message is not a MC Brand plugin message");
byte[] rewrittenData;
ByteBuf rewrittenBuf = Unpooled.buffer();
try {
String currentBrand = ProtocolUtils.readString(Unpooled.wrappedBuffer(message.getData()));
ProtocolUtils.writeString(rewrittenBuf, currentBrand + " (Velocity)");
rewrittenData = new byte[rewrittenBuf.readableBytes()];
rewrittenBuf.readBytes(rewrittenData);
} finally {
rewrittenBuf.release();
}
public static boolean isMCBrand(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals(BRAND_CHANNEL_LEGACY) || message.getChannel().equals(BRAND_CHANNEL);
}
public static boolean isMCRegister(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals(REGISTER_CHANNEL_LEGACY) || message.getChannel().equals(REGISTER_CHANNEL);
}
public static boolean isMCUnregister(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals(UNREGISTER_CHANNEL_LEGACY) || message.getChannel().equals(UNREGISTER_CHANNEL);
}
public static List<String> getChannels(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkArgument(isMCRegister(message) || isMCUnregister(message),"Unknown channel type %s",
message.getChannel());
String channels = new String(message.getData(), StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0"));
}
public static PluginMessage constructChannelsPacket(int protocolVersion, Collection<String> channels) {
Preconditions.checkNotNull(channels, "channels");
String channelName = protocolVersion >= ProtocolConstants.MINECRAFT_1_13 ? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
PluginMessage message = new PluginMessage();
message.setChannel(channelName);
message.setData(String.join("\0", channels).getBytes(StandardCharsets.UTF_8));
return message;
}
public static PluginMessage rewriteMCBrand(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkArgument(isMCBrand(message), "message is not a MC Brand plugin message");
byte[] rewrittenData;
ByteBuf rewrittenBuf = Unpooled.buffer();
try {
String currentBrand = ProtocolUtils.readString(Unpooled.wrappedBuffer(message.getData()));
ProtocolUtils.writeString(rewrittenBuf, currentBrand + " (Velocity)");
rewrittenData = new byte[rewrittenBuf.readableBytes()];
rewrittenBuf.readBytes(rewrittenData);
} finally {
rewrittenBuf.release();
}
PluginMessage newMsg = new PluginMessage();
newMsg.setChannel(message.getChannel());
newMsg.setData(rewrittenData);
return newMsg;
}
PluginMessage newMsg = new PluginMessage();
newMsg.setChannel(message.getChannel());
newMsg.setData(rewrittenData);
return newMsg;
}
}

View File

@@ -9,174 +9,184 @@ import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.scheduler.ScheduledTask;
import com.velocitypowered.api.scheduler.Scheduler;
import com.velocitypowered.api.scheduler.TaskStatus;
import java.util.Collection;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Collection;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.concurrent.*;
public class VelocityScheduler implements Scheduler {
private final PluginManager pluginManager;
private final ExecutorService taskService;
private final ScheduledExecutorService timerExecutionService;
private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedMultimap(
Multimaps.newSetMultimap(new IdentityHashMap<>(), HashSet::new));
public VelocityScheduler(PluginManager pluginManager) {
this.pluginManager = pluginManager;
this.taskService = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setDaemon(true)
.setNameFormat("Velocity Task Scheduler - #%d").build());
this.timerExecutionService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setDaemon(true)
.setNameFormat("Velocity Task Scheduler Timer").build());
private final PluginManager pluginManager;
private final ExecutorService taskService;
private final ScheduledExecutorService timerExecutionService;
private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedMultimap(
Multimaps.newSetMultimap(new IdentityHashMap<>(), HashSet::new));
public VelocityScheduler(PluginManager pluginManager) {
this.pluginManager = pluginManager;
this.taskService = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setDaemon(true)
.setNameFormat("Velocity Task Scheduler - #%d").build());
this.timerExecutionService = Executors
.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setDaemon(true)
.setNameFormat("Velocity Task Scheduler Timer").build());
}
@Override
public TaskBuilder buildTask(Object plugin, Runnable runnable) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(runnable, "runnable");
Preconditions
.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "plugin is not registered");
return new TaskBuilderImpl(plugin, runnable);
}
public boolean shutdown() throws InterruptedException {
Collection<ScheduledTask> terminating;
synchronized (tasksByPlugin) {
terminating = ImmutableList.copyOf(tasksByPlugin.values());
}
for (ScheduledTask task : terminating) {
task.cancel();
}
timerExecutionService.shutdown();
taskService.shutdown();
return taskService.awaitTermination(10, TimeUnit.SECONDS);
}
private class TaskBuilderImpl implements TaskBuilder {
private final Object plugin;
private final Runnable runnable;
private long delay; // ms
private long repeat; // ms
private TaskBuilderImpl(Object plugin, Runnable runnable) {
this.plugin = plugin;
this.runnable = runnable;
}
@Override
public TaskBuilder buildTask(Object plugin, Runnable runnable) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(runnable, "runnable");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "plugin is not registered");
return new TaskBuilderImpl(plugin, runnable);
public TaskBuilder delay(long time, TimeUnit unit) {
this.delay = unit.toMillis(time);
return this;
}
public boolean shutdown() throws InterruptedException {
Collection<ScheduledTask> terminating;
synchronized (tasksByPlugin) {
terminating = ImmutableList.copyOf(tasksByPlugin.values());
}
for (ScheduledTask task : terminating) {
task.cancel();
}
timerExecutionService.shutdown();
taskService.shutdown();
return taskService.awaitTermination(10, TimeUnit.SECONDS);
@Override
public TaskBuilder repeat(long time, TimeUnit unit) {
this.repeat = unit.toMillis(time);
return this;
}
private class TaskBuilderImpl implements TaskBuilder {
private final Object plugin;
private final Runnable runnable;
private long delay; // ms
private long repeat; // ms
private TaskBuilderImpl(Object plugin, Runnable runnable) {
this.plugin = plugin;
this.runnable = runnable;
}
@Override
public TaskBuilder delay(long time, TimeUnit unit) {
this.delay = unit.toMillis(time);
return this;
}
@Override
public TaskBuilder repeat(long time, TimeUnit unit) {
this.repeat = unit.toMillis(time);
return this;
}
@Override
public TaskBuilder clearDelay() {
this.delay = 0;
return this;
}
@Override
public TaskBuilder clearRepeat() {
this.repeat = 0;
return this;
}
@Override
public ScheduledTask schedule() {
VelocityTask task = new VelocityTask(plugin, runnable, delay, repeat);
tasksByPlugin.put(plugin, task);
task.schedule();
return task;
}
@Override
public TaskBuilder clearDelay() {
this.delay = 0;
return this;
}
private class VelocityTask implements Runnable, ScheduledTask {
private final Object plugin;
private final Runnable runnable;
private final long delay;
private final long repeat;
private @Nullable ScheduledFuture<?> future;
private volatile @Nullable Thread currentTaskThread;
private VelocityTask(Object plugin, Runnable runnable, long delay, long repeat) {
this.plugin = plugin;
this.runnable = runnable;
this.delay = delay;
this.repeat = repeat;
}
public void schedule() {
if (repeat == 0) {
this.future = timerExecutionService.schedule(this, delay, TimeUnit.MILLISECONDS);
} else {
this.future = timerExecutionService.scheduleAtFixedRate(this, delay, repeat, TimeUnit.MILLISECONDS);
}
}
@Override
public Object plugin() {
return plugin;
}
@Override
public TaskStatus status() {
if (future == null) {
return TaskStatus.SCHEDULED;
}
if (future.isCancelled()) {
return TaskStatus.CANCELLED;
}
if (future.isDone()) {
return TaskStatus.FINISHED;
}
return TaskStatus.SCHEDULED;
}
@Override
public void cancel() {
if (future != null) {
future.cancel(false);
Thread cur = currentTaskThread;
if (cur != null) {
cur.interrupt();
}
onFinish();
}
}
@Override
public void run() {
taskService.execute(() -> {
currentTaskThread = Thread.currentThread();
try {
runnable.run();
} catch (Exception e) {
Log.logger.error("Exception in task {} by plugin {}", runnable, plugin);
} finally {
currentTaskThread = null;
}
});
}
private void onFinish() {
tasksByPlugin.remove(plugin, this);
}
@Override
public TaskBuilder clearRepeat() {
this.repeat = 0;
return this;
}
private static class Log {
private static final Logger logger = LogManager.getLogger(VelocityTask.class);
@Override
public ScheduledTask schedule() {
VelocityTask task = new VelocityTask(plugin, runnable, delay, repeat);
tasksByPlugin.put(plugin, task);
task.schedule();
return task;
}
}
private class VelocityTask implements Runnable, ScheduledTask {
private final Object plugin;
private final Runnable runnable;
private final long delay;
private final long repeat;
private @Nullable ScheduledFuture<?> future;
private volatile @Nullable Thread currentTaskThread;
private VelocityTask(Object plugin, Runnable runnable, long delay, long repeat) {
this.plugin = plugin;
this.runnable = runnable;
this.delay = delay;
this.repeat = repeat;
}
public void schedule() {
if (repeat == 0) {
this.future = timerExecutionService.schedule(this, delay, TimeUnit.MILLISECONDS);
} else {
this.future = timerExecutionService
.scheduleAtFixedRate(this, delay, repeat, TimeUnit.MILLISECONDS);
}
}
@Override
public Object plugin() {
return plugin;
}
@Override
public TaskStatus status() {
if (future == null) {
return TaskStatus.SCHEDULED;
}
if (future.isCancelled()) {
return TaskStatus.CANCELLED;
}
if (future.isDone()) {
return TaskStatus.FINISHED;
}
return TaskStatus.SCHEDULED;
}
@Override
public void cancel() {
if (future != null) {
future.cancel(false);
Thread cur = currentTaskThread;
if (cur != null) {
cur.interrupt();
}
onFinish();
}
}
@Override
public void run() {
taskService.execute(() -> {
currentTaskThread = Thread.currentThread();
try {
runnable.run();
} catch (Exception e) {
Log.logger.error("Exception in task {} by plugin {}", runnable, plugin);
} finally {
currentTaskThread = null;
}
});
}
private void onFinish() {
tasksByPlugin.remove(plugin, this);
}
}
private static class Log {
private static final Logger logger = LogManager.getLogger(VelocityTask.class);
}
}

View File

@@ -5,55 +5,59 @@ import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.VelocityServer;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.checkerframework.checker.nullness.qual.Nullable;
public class ServerMap {
private final @Nullable VelocityServer server;
private final Map<String, RegisteredServer> servers = new ConcurrentHashMap<>();
public ServerMap(@Nullable VelocityServer server) {
this.server = server;
private final @Nullable VelocityServer server;
private final Map<String, RegisteredServer> servers = new ConcurrentHashMap<>();
public ServerMap(@Nullable VelocityServer server) {
this.server = server;
}
public Optional<RegisteredServer> getServer(String name) {
Preconditions.checkNotNull(name, "server");
String lowerName = name.toLowerCase(Locale.US);
return Optional.ofNullable(servers.get(lowerName));
}
public Collection<RegisteredServer> getAllServers() {
return ImmutableList.copyOf(servers.values());
}
public RegisteredServer register(ServerInfo serverInfo) {
Preconditions.checkNotNull(serverInfo, "serverInfo");
String lowerName = serverInfo.getName().toLowerCase(Locale.US);
VelocityRegisteredServer rs = new VelocityRegisteredServer(server, serverInfo);
RegisteredServer existing = servers.putIfAbsent(lowerName, rs);
if (existing != null && !existing.getServerInfo().equals(serverInfo)) {
throw new IllegalArgumentException(
"Server with name " + serverInfo.getName() + " already registered");
} else if (existing == null) {
return rs;
} else {
return existing;
}
}
public Optional<RegisteredServer> getServer(String name) {
Preconditions.checkNotNull(name, "server");
String lowerName = name.toLowerCase(Locale.US);
return Optional.ofNullable(servers.get(lowerName));
}
public Collection<RegisteredServer> getAllServers() {
return ImmutableList.copyOf(servers.values());
}
public RegisteredServer register(ServerInfo serverInfo) {
Preconditions.checkNotNull(serverInfo, "serverInfo");
String lowerName = serverInfo.getName().toLowerCase(Locale.US);
VelocityRegisteredServer rs = new VelocityRegisteredServer(server, serverInfo);
RegisteredServer existing = servers.putIfAbsent(lowerName, rs);
if (existing != null && !existing.getServerInfo().equals(serverInfo)) {
throw new IllegalArgumentException("Server with name " + serverInfo.getName() + " already registered");
} else if (existing == null) {
return rs;
} else {
return existing;
}
}
public void unregister(ServerInfo serverInfo) {
Preconditions.checkNotNull(serverInfo, "serverInfo");
String lowerName = serverInfo.getName().toLowerCase(Locale.US);
RegisteredServer rs = servers.get(lowerName);
if (rs == null) {
throw new IllegalArgumentException("Server with name " + serverInfo.getName() + " is not registered!");
}
Preconditions.checkArgument(rs.getServerInfo().equals(serverInfo), "Trying to remove server %s with differing information", serverInfo.getName());
Preconditions.checkState(servers.remove(lowerName, rs), "Server with name %s replaced whilst unregistering", serverInfo.getName());
public void unregister(ServerInfo serverInfo) {
Preconditions.checkNotNull(serverInfo, "serverInfo");
String lowerName = serverInfo.getName().toLowerCase(Locale.US);
RegisteredServer rs = servers.get(lowerName);
if (rs == null) {
throw new IllegalArgumentException(
"Server with name " + serverInfo.getName() + " is not registered!");
}
Preconditions.checkArgument(rs.getServerInfo().equals(serverInfo),
"Trying to remove server %s with differing information", serverInfo.getName());
Preconditions.checkState(servers.remove(lowerName, rs),
"Server with name %s replaced whilst unregistering", serverInfo.getName());
}
}

View File

@@ -1,5 +1,12 @@
package com.velocitypowered.proxy.server;
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.HANDLER;
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.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.proxy.Player;
@@ -23,95 +30,98 @@ import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.timeout.ReadTimeoutHandler;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*;
import org.checkerframework.checker.nullness.qual.Nullable;
public class VelocityRegisteredServer implements RegisteredServer {
private final @Nullable VelocityServer server;
private final ServerInfo serverInfo;
private final Set<ConnectedPlayer> players = ConcurrentHashMap.newKeySet();
public VelocityRegisteredServer(@Nullable VelocityServer server, ServerInfo serverInfo) {
this.server = server;
this.serverInfo = Preconditions.checkNotNull(serverInfo, "serverInfo");
private final @Nullable VelocityServer server;
private final ServerInfo serverInfo;
private final Set<ConnectedPlayer> players = ConcurrentHashMap.newKeySet();
public VelocityRegisteredServer(@Nullable VelocityServer server, ServerInfo serverInfo) {
this.server = server;
this.serverInfo = Preconditions.checkNotNull(serverInfo, "serverInfo");
}
@Override
public ServerInfo getServerInfo() {
return serverInfo;
}
@Override
public Collection<Player> getPlayersConnected() {
return ImmutableList.copyOf(players);
}
@Override
public CompletableFuture<ServerPing> ping() {
if (server == null) {
throw new IllegalStateException("No Velocity proxy instance available");
}
CompletableFuture<ServerPing> pingFuture = new CompletableFuture<>();
server.initializeGenericBootstrap()
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(READ_TIMEOUT,
new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(),
TimeUnit.SECONDS))
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER,
new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
.addLast(MINECRAFT_ENCODER,
new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
@Override
public ServerInfo getServerInfo() {
return serverInfo;
}
@Override
public Collection<Player> getPlayersConnected() {
return ImmutableList.copyOf(players);
}
@Override
public CompletableFuture<ServerPing> ping() {
if (server == null) {
throw new IllegalStateException("No Velocity proxy instance available");
}
CompletableFuture<ServerPing> pingFuture = new CompletableFuture<>();
server.initializeGenericBootstrap()
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS))
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
MinecraftConnection connection = new MinecraftConnection(ch, server);
connection.setState(StateRegistry.HANDSHAKE);
ch.pipeline().addLast(HANDLER, connection);
}
})
.connect(serverInfo.getAddress())
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
conn.setSessionHandler(new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn));
} else {
pingFuture.completeExceptionally(future.cause());
}
}
});
return pingFuture;
}
public void addPlayer(ConnectedPlayer player) {
players.add(player);
}
public void removePlayer(ConnectedPlayer player) {
players.remove(player);
}
@Override
public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
for (ConnectedPlayer player : players) {
ServerConnection connection = player.getConnectedServer();
if (connection != null && connection.getServerInfo().equals(serverInfo)) {
return connection.sendPluginMessage(identifier, data);
MinecraftConnection connection = new MinecraftConnection(ch, server);
connection.setState(StateRegistry.HANDSHAKE);
ch.pipeline().addLast(HANDLER, connection);
}
})
.connect(serverInfo.getAddress())
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
conn.setSessionHandler(
new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn));
} else {
pingFuture.completeExceptionally(future.cause());
}
}
}
});
return pingFuture;
}
return false;
public void addPlayer(ConnectedPlayer player) {
players.add(player);
}
public void removePlayer(ConnectedPlayer player) {
players.remove(player);
}
@Override
public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
for (ConnectedPlayer player : players) {
ServerConnection connection = player.getConnectedServer();
if (connection != null && connection.getServerInfo().equals(serverInfo)) {
return connection.sendPluginMessage(identifier, data);
}
}
@Override
public String toString() {
return "registered server: " + serverInfo;
}
return false;
}
@Override
public String toString() {
return "registered server: " + serverInfo;
}
}

View File

@@ -10,56 +10,57 @@ import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
public class PingSessionHandler implements MinecraftSessionHandler {
private final CompletableFuture<ServerPing> result;
private final RegisteredServer server;
private final MinecraftConnection connection;
private boolean completed = false;
public PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server, MinecraftConnection connection) {
this.result = result;
this.server = server;
this.connection = connection;
private final CompletableFuture<ServerPing> result;
private final RegisteredServer server;
private final MinecraftConnection connection;
private boolean completed = false;
public PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server,
MinecraftConnection connection) {
this.result = result;
this.server = server;
this.connection = connection;
}
@Override
public void activated() {
Handshake handshake = new Handshake();
handshake.setNextStatus(StateRegistry.STATUS_ID);
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString());
handshake.setPort(server.getServerInfo().getAddress().getPort());
handshake.setProtocolVersion(ProtocolConstants.MINIMUM_GENERIC_VERSION);
connection.write(handshake);
connection.setState(StateRegistry.STATUS);
connection.write(StatusRequest.INSTANCE);
}
@Override
public boolean handle(StatusResponse packet) {
// All good!
completed = true;
connection.close();
ServerPing ping = VelocityServer.GSON.fromJson(packet.getStatus(), ServerPing.class);
result.complete(ping);
return true;
}
@Override
public void disconnected() {
if (!completed) {
result.completeExceptionally(new IOException("Unexpectedly disconnected from remote server"));
}
}
@Override
public void activated() {
Handshake handshake = new Handshake();
handshake.setNextStatus(StateRegistry.STATUS_ID);
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString());
handshake.setPort(server.getServerInfo().getAddress().getPort());
handshake.setProtocolVersion(ProtocolConstants.MINIMUM_GENERIC_VERSION);
connection.write(handshake);
connection.setState(StateRegistry.STATUS);
connection.write(StatusRequest.INSTANCE);
}
@Override
public boolean handle(StatusResponse packet) {
// All good!
completed = true;
connection.close();
ServerPing ping = VelocityServer.GSON.fromJson(packet.getStatus(), ServerPing.class);
result.complete(ping);
return true;
}
@Override
public void disconnected() {
if (!completed) {
result.completeExceptionally(new IOException("Unexpectedly disconnected from remote server"));
}
}
@Override
public void exception(Throwable throwable) {
completed = true;
result.completeExceptionally(throwable);
}
@Override
public void exception(Throwable throwable) {
completed = true;
result.completeExceptionally(throwable);
}
}

View File

@@ -8,131 +8,142 @@ import com.velocitypowered.api.util.UuidUtils;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter;
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class VelocityTabList implements TabList {
private final MinecraftConnection connection;
private final Map<UUID, TabListEntry> entries = new ConcurrentHashMap<>();
public VelocityTabList(MinecraftConnection connection) {
this.connection = connection;
private final MinecraftConnection connection;
private final Map<UUID, TabListEntry> entries = new ConcurrentHashMap<>();
public VelocityTabList(MinecraftConnection connection) {
this.connection = connection;
}
@Override
public void setHeaderAndFooter(Component header, Component footer) {
Preconditions.checkNotNull(header, "header");
Preconditions.checkNotNull(footer, "footer");
connection.write(HeaderAndFooter.create(header, footer));
}
@Override
public void clearHeaderAndFooter() {
connection.write(HeaderAndFooter.reset());
}
@Override
public void addEntry(TabListEntry entry) {
Preconditions.checkNotNull(entry, "entry");
Preconditions.checkArgument(entry.getTabList().equals(this),
"The provided entry was not created by this tab list");
Preconditions.checkArgument(!entries.containsKey(entry.getProfile().idAsUuid()),
"this TabList already contains an entry with the same uuid");
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
connection.write(
new PlayerListItem(PlayerListItem.ADD_PLAYER, Collections.singletonList(packetItem)));
entries.put(entry.getProfile().idAsUuid(), entry);
}
@Override
public Optional<TabListEntry> removeEntry(UUID uuid) {
TabListEntry entry = entries.remove(uuid);
if (entry != null) {
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
connection.write(
new PlayerListItem(PlayerListItem.REMOVE_PLAYER, Collections.singletonList(packetItem)));
}
@Override
public void setHeaderAndFooter(Component header, Component footer) {
Preconditions.checkNotNull(header, "header");
Preconditions.checkNotNull(footer, "footer");
connection.write(HeaderAndFooter.create(header, footer));
return Optional.ofNullable(entry);
}
public void clearAll() { // Note: this method is called upon server switch
List<PlayerListItem.Item> items = new ArrayList<>();
for (TabListEntry value : entries.values()) {
items.add(PlayerListItem.Item.from(value));
}
entries.clear();
connection.delayedWrite(new PlayerListItem(PlayerListItem.REMOVE_PLAYER, items));
}
@Override
public void clearHeaderAndFooter() {
connection.write(HeaderAndFooter.reset());
}
@Override
public Collection<TabListEntry> getEntries() {
return Collections.unmodifiableCollection(this.entries.values());
}
@Override
public void addEntry(TabListEntry entry) {
Preconditions.checkNotNull(entry, "entry");
Preconditions.checkArgument(entry.getTabList().equals(this), "The provided entry was not created by this tab list");
Preconditions.checkArgument(!entries.containsKey(entry.getProfile().idAsUuid()), "this TabList already contains an entry with the same uuid");
@Override
public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
int gameMode) {
return new VelocityTabListEntry(this, profile, displayName, latency, gameMode);
}
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
connection.write(new PlayerListItem(PlayerListItem.ADD_PLAYER, Collections.singletonList(packetItem)));
entries.put(entry.getProfile().idAsUuid(), entry);
}
public void processBackendPacket(PlayerListItem packet) {
//Packets are already forwarded on, so no need to do that here
for (PlayerListItem.Item item : packet.getItems()) {
UUID uuid = item.getUuid();
if (packet.getAction() != PlayerListItem.ADD_PLAYER && !entries.containsKey(uuid)) {
//Sometimes UPDATE_GAMEMODE is sent before ADD_PLAYER so don't want to warn here
continue;
}
@Override
public Optional<TabListEntry> removeEntry(UUID uuid) {
TabListEntry entry = entries.remove(uuid);
if (entry != null) {
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
connection.write(new PlayerListItem(PlayerListItem.REMOVE_PLAYER, Collections.singletonList(packetItem)));
switch (packet.getAction()) {
case PlayerListItem.ADD_PLAYER: {
// ensure that name and properties are available
String name = item.getName();
List<GameProfile.Property> properties = item.getProperties();
if (name == null || properties == null) {
throw new IllegalStateException("Got null game profile for ADD_PLAYER");
}
entries.put(item.getUuid(), TabListEntry.builder()
.tabList(this)
.profile(new GameProfile(UuidUtils.toUndashed(uuid), name, properties))
.displayName(item.getDisplayName())
.latency(item.getLatency())
.gameMode(item.getGameMode())
.build());
break;
}
return Optional.ofNullable(entry);
}
public void clearAll() { // Note: this method is called upon server switch
List<PlayerListItem.Item> items = new ArrayList<>();
for (TabListEntry value : entries.values()) {
items.add(PlayerListItem.Item.from(value));
case PlayerListItem.REMOVE_PLAYER:
entries.remove(uuid);
break;
case PlayerListItem.UPDATE_DISPLAY_NAME: {
TabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setDisplayName(item.getDisplayName());
}
break;
}
entries.clear();
connection.delayedWrite(new PlayerListItem(PlayerListItem.REMOVE_PLAYER, items));
}
@Override
public Collection<TabListEntry> getEntries() {
return Collections.unmodifiableCollection(this.entries.values());
}
@Override
public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode) {
return new VelocityTabListEntry(this, profile, displayName, latency, gameMode);
}
public void processBackendPacket(PlayerListItem packet) {
//Packets are already forwarded on, so no need to do that here
for (PlayerListItem.Item item : packet.getItems()) {
UUID uuid = item.getUuid();
if (packet.getAction() != PlayerListItem.ADD_PLAYER && !entries.containsKey(uuid)) {
//Sometimes UPDATE_GAMEMODE is sent before ADD_PLAYER so don't want to warn here
continue;
}
switch (packet.getAction()) {
case PlayerListItem.ADD_PLAYER: {
// ensure that name and properties are available
String name = item.getName();
List<GameProfile.Property> properties = item.getProperties();
if (name == null || properties == null) {
throw new IllegalStateException("Got null game profile for ADD_PLAYER");
}
entries.put(item.getUuid(), TabListEntry.builder()
.tabList(this)
.profile(new GameProfile(UuidUtils.toUndashed(uuid), name, properties))
.displayName(item.getDisplayName())
.latency(item.getLatency())
.gameMode(item.getGameMode())
.build());
break;
}
case PlayerListItem.REMOVE_PLAYER:
entries.remove(uuid);
break;
case PlayerListItem.UPDATE_DISPLAY_NAME: {
TabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setDisplayName(item.getDisplayName());
}
break;
}
case PlayerListItem.UPDATE_LATENCY: {
TabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setLatency(item.getLatency());
}
break;
}
case PlayerListItem.UPDATE_GAMEMODE: {
TabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setLatency(item.getGameMode());
}
break;
}
}
case PlayerListItem.UPDATE_LATENCY: {
TabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setLatency(item.getLatency());
}
break;
}
}
void updateEntry(int action, TabListEntry entry) {
if (entries.containsKey(entry.getProfile().idAsUuid())) {
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
connection.write(new PlayerListItem(action, Collections.singletonList(packetItem)));
case PlayerListItem.UPDATE_GAMEMODE: {
TabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setLatency(item.getGameMode());
}
break;
}
}
}
}
void updateEntry(int action, TabListEntry entry) {
if (entries.containsKey(entry.getProfile().idAsUuid())) {
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
connection.write(new PlayerListItem(action, Collections.singletonList(packetItem)));
}
}
}

View File

@@ -4,69 +4,70 @@ import com.velocitypowered.api.proxy.player.TabList;
import com.velocitypowered.api.proxy.player.TabListEntry;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
import java.util.Optional;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Optional;
public class VelocityTabListEntry implements TabListEntry {
private final VelocityTabList tabList;
private final GameProfile profile;
private @Nullable Component displayName;
private int latency;
private int gameMode;
VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, @Nullable Component displayName, int latency, int gameMode) {
this.tabList = tabList;
this.profile = profile;
this.displayName = displayName;
this.latency = latency;
this.gameMode = gameMode;
}
private final VelocityTabList tabList;
private final GameProfile profile;
private @Nullable Component displayName;
private int latency;
private int gameMode;
@Override
public TabList getTabList() {
return tabList;
}
VelocityTabListEntry(VelocityTabList tabList, GameProfile profile,
@Nullable Component displayName, int latency, int gameMode) {
this.tabList = tabList;
this.profile = profile;
this.displayName = displayName;
this.latency = latency;
this.gameMode = gameMode;
}
@Override
public GameProfile getProfile() {
return profile;
}
@Override
public TabList getTabList() {
return tabList;
}
@Override
public Optional<Component> getDisplayName() {
return Optional.ofNullable(displayName);
}
@Override
public GameProfile getProfile() {
return profile;
}
@Override
public TabListEntry setDisplayName(@Nullable Component displayName) {
this.displayName = displayName;
tabList.updateEntry(PlayerListItem.UPDATE_DISPLAY_NAME, this);
return this;
}
@Override
public Optional<Component> getDisplayName() {
return Optional.ofNullable(displayName);
}
@Override
public int getLatency() {
return latency;
}
@Override
public TabListEntry setDisplayName(@Nullable Component displayName) {
this.displayName = displayName;
tabList.updateEntry(PlayerListItem.UPDATE_DISPLAY_NAME, this);
return this;
}
@Override
public TabListEntry setLatency(int latency) {
this.latency = latency;
tabList.updateEntry(PlayerListItem.UPDATE_LATENCY, this);
return this;
}
@Override
public int getLatency() {
return latency;
}
@Override
public int getGameMode() {
return gameMode;
}
@Override
public TabListEntry setLatency(int latency) {
this.latency = latency;
tabList.updateEntry(PlayerListItem.UPDATE_LATENCY, this);
return this;
}
@Override
public TabListEntry setGameMode(int gameMode) {
this.gameMode = gameMode;
tabList.updateEntry(PlayerListItem.UPDATE_GAMEMODE, this);
return this;
}
@Override
public int getGameMode() {
return gameMode;
}
@Override
public TabListEntry setGameMode(int gameMode) {
this.gameMode = gameMode;
tabList.updateEntry(PlayerListItem.UPDATE_GAMEMODE, this);
return this;
}
}

View File

@@ -1,16 +1,15 @@
package com.velocitypowered.proxy.util;
import com.google.common.base.Preconditions;
import java.net.InetSocketAddress;
import java.net.URI;
public enum AddressUtil {
;
;
public static InetSocketAddress parseAddress(String ip) {
Preconditions.checkNotNull(ip, "ip");
URI uri = URI.create("tcp://" + ip);
return new InetSocketAddress(uri.getHost(), uri.getPort());
}
public static InetSocketAddress parseAddress(String ip) {
Preconditions.checkNotNull(ip, "ip");
URI uri = URI.create("tcp://" + ip);
return new InetSocketAddress(uri.getHost(), uri.getPort());
}
}

View File

@@ -1,40 +1,45 @@
package com.velocitypowered.proxy.util;
import javax.crypto.Cipher;
import java.math.BigInteger;
import java.security.*;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import javax.crypto.Cipher;
public enum EncryptionUtils {
;
;
public static KeyPair createRsaKeyPair(final int keysize) {
try {
final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(keysize);
return generator.generateKeyPair();
} catch (final NoSuchAlgorithmException e) {
throw new RuntimeException("Unable to generate RSA keypair", e);
}
public static KeyPair createRsaKeyPair(final int keysize) {
try {
final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(keysize);
return generator.generateKeyPair();
} catch (final NoSuchAlgorithmException e) {
throw new RuntimeException("Unable to generate RSA keypair", e);
}
}
public static String twosComplementHexdigest(byte[] digest) {
return new BigInteger(digest).toString(16);
}
public static String twosComplementHexdigest(byte[] digest) {
return new BigInteger(digest).toString(16);
}
public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
return cipher.doFinal(bytes);
}
public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
return cipher.doFinal(bytes);
}
public static String generateServerId(byte[] sharedSecret, PublicKey key) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(sharedSecret);
digest.update(key.getEncoded());
return twosComplementHexdigest(digest.digest());
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
public static String generateServerId(byte[] sharedSecret, PublicKey key) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(sharedSecret);
digest.update(key.getEncoded());
return twosComplementHexdigest(digest.digest());
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

Some files were not shown because too many files have changed in this diff Show More