Merge branch 'master' into native-crypto

# Conflicts:
#	native/src/main/java/com/velocitypowered/natives/util/Natives.java
This commit is contained in:
Andrew Steinborn
2018-08-25 01:12:26 -04:00
152 changed files with 6546 additions and 866 deletions

View File

@@ -1,6 +1,7 @@
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '2.0.4'
id 'de.sebastianboegl.shadow.transformer.log4j' version '2.1.1'
}
compileJava {
@@ -21,17 +22,70 @@ jar {
dependencies {
compile project(':velocity-api')
compile project(':velocity-native')
compile "io.netty:netty-codec:${nettyVersion}"
compile "io.netty:netty-codec-http:${nettyVersion}"
compile "io.netty:netty-handler:${nettyVersion}"
compile "io.netty:netty-transport-native-epoll:${nettyVersion}"
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
compile "org.apache.logging.log4j:log4j-core:${log4jVersion}"
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}"
compile 'net.minecrell:terminalconsoleappender:1.1.1'
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
compile 'it.unimi.dsi:fastutil:8.2.1'
compile 'net.kyori:event-method-asm:3.0.0'
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
}
shadowJar {
exclude 'it/unimi/dsi/fastutil/booleans/**'
exclude 'it/unimi/dsi/fastutil/bytes/**'
exclude 'it/unimi/dsi/fastutil/chars/**'
exclude 'it/unimi/dsi/fastutil/doubles/**'
exclude 'it/unimi/dsi/fastutil/floats/**'
exclude 'it/unimi/dsi/fastutil/ints/*Int2*'
exclude 'it/unimi/dsi/fastutil/ints/IntAVL*'
exclude 'it/unimi/dsi/fastutil/ints/IntArray*'
exclude 'it/unimi/dsi/fastutil/ints/IntBi*'
exclude 'it/unimi/dsi/fastutil/ints/IntList*'
exclude 'it/unimi/dsi/fastutil/ints/IntOpen*'
exclude 'it/unimi/dsi/fastutil/ints/IntRB*'
exclude 'it/unimi/dsi/fastutil/ints/IntSet*'
exclude 'it/unimi/dsi/fastutil/ints/IntSorted*'
exclude 'it/unimi/dsi/fastutil/io/**'
exclude 'it/unimi/dsi/fastutil/longs/**'
exclude 'it/unimi/dsi/fastutil/objects/*ObjectArray*'
exclude 'it/unimi/dsi/fastutil/objects/*ObjectAVL*'
exclude 'it/unimi/dsi/fastutil/objects/*Object*Big*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Boolean*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Byte*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Char*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Double*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Float*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntArray*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntAVL*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntLinked*'
exclude 'it/unimi/dsi/fastutil/objects/*Object*OpenCustom*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntRB*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntSorted*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Long*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Object*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Reference*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2Short*'
exclude 'it/unimi/dsi/fastutil/objects/*ObjectRB*'
exclude 'it/unimi/dsi/fastutil/objects/*ObjectSorted*'
exclude 'it/unimi/dsi/fastutil/objects/*Reference*'
exclude 'it/unimi/dsi/fastutil/shorts/**'
}
artifacts {
archives shadowJar
}
}

View File

@@ -1,12 +1,33 @@
package com.velocitypowered.proxy;
import com.velocitypowered.proxy.console.VelocityConsole;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.text.DecimalFormat;
public class Velocity {
public static void main(String... args) throws InterruptedException {
private static final Logger logger = LogManager.getLogger(Velocity.class);
private static long startTime;
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");
}
public static void main(String... args) {
startTime = System.currentTimeMillis();
logger.info("Booting up Velocity...");
final VelocityServer server = VelocityServer.getServer();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown, "Shutdown thread"));
Thread.currentThread().join();
double bootTime = (System.currentTimeMillis() - startTime) / 1000d;
logger.info("Done ({}s)!", new DecimalFormat("#.##").format(bootTime));
new VelocityConsole(server).start();
}
}

View File

@@ -1,17 +1,40 @@
package com.velocitypowered.proxy;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.network.ConnectionManager;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.network.ConnectionManager;
import com.velocitypowered.proxy.command.ServerCommand;
import com.velocitypowered.proxy.command.ShutdownCommand;
import com.velocitypowered.proxy.command.VelocityCommand;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.command.VelocityCommandManager;
import com.velocitypowered.proxy.messages.VelocityChannelRegistrar;
import com.velocitypowered.proxy.plugin.VelocityEventManager;
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
import com.velocitypowered.proxy.scheduler.Sleeper;
import com.velocitypowered.proxy.scheduler.VelocityScheduler;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.Ratelimiter;
import com.velocitypowered.proxy.util.ServerMap;
import io.netty.bootstrap.Bootstrap;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.serializer.ComponentSerializers;
import net.kyori.text.serializer.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -22,13 +45,17 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public class VelocityServer {
public class VelocityServer implements ProxyServer {
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
private static final VelocityServer INSTANCE = new VelocityServer();
public static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
.registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer())
.create();
private final ConnectionManager cm = new ConnectionManager();
@@ -36,8 +63,33 @@ public class VelocityServer {
private NettyHttpClient httpClient;
private KeyPair serverKeyPair;
private final ServerMap servers = new ServerMap();
private final VelocityCommandManager commandManager = new VelocityCommandManager();
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
private boolean shutdown = false;
private final VelocityPluginManager pluginManager = new VelocityPluginManager(this);
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private final CommandSource consoleCommandSource = new CommandSource() {
@Override
public void sendMessage(Component component) {
logger.info(ComponentSerializers.LEGACY.serialize(component));
}
@Override
public boolean hasPermission(String permission) {
return true;
}
};
private Ratelimiter ipAttemptLimiter;
private VelocityEventManager eventManager;
private VelocityScheduler scheduler;
private VelocityChannelRegistrar channelRegistrar;
private VelocityServer() {
commandManager.register(new VelocityCommand(), "velocity");
commandManager.register(new ServerCommand(), "server");
commandManager.register(new ShutdownCommand(), "shutdown");
}
public static VelocityServer getServer() {
@@ -52,12 +104,12 @@ public class VelocityServer {
return configuration;
}
public void start() {
logger.info("Using {}", Natives.compressor.getLoadedVariant());
logger.info("Using {}", Natives.cipher.getLoadedVariant());
@Override
public VelocityCommandManager getCommandManager() {
return commandManager;
}
// Create a key pair
logger.info("Booting up Velocity...");
public void start() {
try {
Path configPath = Paths.get("velocity.toml");
try {
@@ -82,10 +134,55 @@ public class VelocityServer {
}
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
ipAttemptLimiter = new Ratelimiter(configuration.getLoginRatelimit());
httpClient = new NettyHttpClient(this);
eventManager = new VelocityEventManager(pluginManager);
scheduler = new VelocityScheduler(pluginManager, Sleeper.SYSTEM);
channelRegistrar = new VelocityChannelRegistrar();
loadPlugins();
try {
// 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()).get();
} catch (InterruptedException | ExecutionException e) {
// Ignore, we don't care. InterruptedException is unlikely to happen (and if it does, you've got bigger
// issues) and there is almost no chance ExecutionException will be thrown.
}
this.cm.bind(configuration.getBind());
if (configuration.isQueryEnabled()) {
this.cm.queryBind(configuration.getBind().getHostString(), configuration.getQueryPort());
}
}
private void loadPlugins() {
logger.info("Loading plugins...");
try {
Path pluginPath = Paths.get("plugins");
if (Files.notExists(pluginPath)) {
Files.createDirectory(pluginPath);
} else {
if (!Files.isDirectory(pluginPath)) {
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
pluginManager.getPlugins().forEach(container -> {
container.getInstance().ifPresent(plugin -> eventManager.register(plugin, plugin));
});
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
}
public ServerMap getServers() {
@@ -96,11 +193,122 @@ public class VelocityServer {
return this.cm.createWorker();
}
public boolean isShutdown() {
return shutdown;
}
public void shutdown() {
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...
}
shutdown = true;
}
public NettyHttpClient getHttpClient() {
return httpClient;
}
public Ratelimiter getIpAttemptLimiter() {
return ipAttemptLimiter;
}
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 void unregisterConnection(ConnectedPlayer connection) {
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
connectionsByUuid.remove(connection.getUniqueId(), connection);
}
@Override
public Optional<Player> getPlayer(String username) {
Preconditions.checkNotNull(username, "username");
return Optional.ofNullable(connectionsByName.get(username.toLowerCase(Locale.US)));
}
@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 int getPlayerCount() {
return connectionsByUuid.size();
}
@Override
public Optional<ServerInfo> getServerInfo(String name) {
Preconditions.checkNotNull(name, "name");
return servers.getServer(name);
}
@Override
public Collection<ServerInfo> getAllServers() {
return servers.getAllServers();
}
@Override
public void registerServer(ServerInfo server) {
servers.register(server);
}
@Override
public void unregisterServer(ServerInfo server) {
servers.unregister(server);
}
@Override
public CommandSource getConsoleCommandSource() {
return consoleCommandSource;
}
@Override
public PluginManager getPluginManager() {
return pluginManager;
}
@Override
public EventManager getEventManager() {
return eventManager;
}
@Override
public VelocityScheduler getScheduler() {
return scheduler;
}
@Override
public VelocityChannelRegistrar getChannelRegistrar() {
return channelRegistrar;
}
}

View File

@@ -0,0 +1,58 @@
package com.velocitypowered.proxy.command;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class ServerCommand implements Command {
@Override
public void execute(CommandSource source, String[] 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<ServerInfo> server = VelocityServer.getServer().getServerInfo(serverName);
if (!server.isPresent()) {
player.sendMessage(TextComponent.of("Server " + serverName + " doesn't exist.", TextColor.RED));
return;
}
player.createConnectionRequest(server.get()).fireAndForget();
} else {
String serverList = VelocityServer.getServer().getAllServers().stream()
.map(ServerInfo::getName)
.collect(Collectors.joining(", "));
player.sendMessage(TextComponent.of("Available servers: " + serverList, TextColor.YELLOW));
}
}
@Override
public List<String> suggest(CommandSource source, String[] currentArgs) {
if (currentArgs.length == 0) {
return VelocityServer.getServer().getAllServers().stream()
.map(ServerInfo::getName)
.collect(Collectors.toList());
} else if (currentArgs.length == 1) {
return VelocityServer.getServer().getAllServers().stream()
.map(ServerInfo::getName)
.filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length()))
.collect(Collectors.toList());
} else {
return ImmutableList.of();
}
}
}

View File

@@ -0,0 +1,18 @@
package com.velocitypowered.proxy.command;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
public class ShutdownCommand implements Command {
@Override
public void execute(CommandSource source, String[] args) {
if (source != VelocityServer.getServer().getConsoleCommandSource()) {
source.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED));
return;
}
VelocityServer.getServer().shutdown();
}
}

View File

@@ -0,0 +1,40 @@
package com.velocitypowered.proxy.command;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.event.ClickEvent;
import net.kyori.text.format.TextColor;
public class VelocityCommand implements Command {
@Override
public void execute(CommandSource source, String[] args) {
String implVersion = VelocityServer.class.getPackage().getImplementationVersion();
TextComponent thisIsVelocity = TextComponent.builder()
.content("This is ")
.append(TextComponent.of("Velocity " + implVersion, TextColor.DARK_AQUA))
.append(TextComponent.of(", the next generation Minecraft: Java Edition proxy.").resetStyle())
.build();
TextComponent velocityInfo = TextComponent.builder()
.content("Copyright 2018 Velocity Contributors. Velocity is freely licensed under the terms of the " +
"MIT License.")
.build();
TextComponent velocityWebsite = TextComponent.builder()
.content("Visit the ")
.append(TextComponent.builder("Velocity website")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.velocitypowered.com"))
.build())
.append(TextComponent.of(" or the ").resetStyle())
.append(TextComponent.builder("Velocity GitHub")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/astei/velocity"))
.build())
.build();
source.sendMessage(thisIsVelocity);
source.sendMessage(velocityInfo);
source.sendMessage(velocityWebsite);
}
}

View File

@@ -0,0 +1,85 @@
package com.velocitypowered.proxy.command;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.CommandManager;
import java.util.*;
import java.util.stream.Collectors;
public class VelocityCommandManager implements CommandManager {
private final Map<String, Command> commands = new HashMap<>();
@Override
public void register(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(final String alias) {
Preconditions.checkNotNull(alias, "name");
this.commands.remove(alias.toLowerCase(Locale.ENGLISH));
}
@Override
public boolean execute(CommandSource source, String cmdLine) {
Preconditions.checkNotNull(source, "invoker");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return false;
}
String alias = split[0];
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
return false;
}
try {
command.execute(source, actualArgs);
return true;
} catch (Exception e) {
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
}
}
public Optional<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 Optional.empty();
}
String command = split[0];
if (split.length == 1) {
return Optional.of(commands.keySet().stream()
.filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length()))
.collect(Collectors.toList()));
}
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command executor = commands.get(command);
if (executor == null) {
return Optional.empty();
}
try {
return Optional.of(executor.suggest(source, actualArgs));
} catch (Exception e) {
throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + source, e);
}
}
}

View File

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

View File

@@ -2,8 +2,10 @@ package com.velocitypowered.proxy.config;
import com.google.common.collect.ImmutableMap;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.api.util.LegacyChatColorUtils;
import io.netty.buffer.ByteBufUtil;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import org.apache.logging.log4j.LogManager;
@@ -15,6 +17,7 @@ import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -26,27 +29,39 @@ public class VelocityConfiguration {
private final String motd;
private final int showMaxPlayers;
private final boolean onlineMode;
private final IPForwardingMode ipForwardingMode;
private final PlayerInfoForwarding playerInfoForwardingMode;
private final Map<String, String> servers;
private final List<String> attemptConnectionOrder;
private final int compressionThreshold;
private final int compressionLevel;
private final int loginRatelimit;
private final boolean queryEnabled;
private final int queryPort;
private Component motdAsComponent;
private Favicon favicon;
private final byte[] forwardingSecret;
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
IPForwardingMode ipForwardingMode, Map<String, String> servers,
PlayerInfoForwarding playerInfoForwardingMode, Map<String, String> servers,
List<String> attemptConnectionOrder, int compressionThreshold,
int compressionLevel) {
int compressionLevel, int loginRatelimit, boolean queryEnabled,
int queryPort, byte[] forwardingSecret) {
this.bind = bind;
this.motd = motd;
this.showMaxPlayers = showMaxPlayers;
this.onlineMode = onlineMode;
this.ipForwardingMode = ipForwardingMode;
this.playerInfoForwardingMode = playerInfoForwardingMode;
this.servers = servers;
this.attemptConnectionOrder = attemptConnectionOrder;
this.compressionThreshold = compressionThreshold;
this.compressionLevel = compressionLevel;
this.loginRatelimit = loginRatelimit;
this.queryEnabled = queryEnabled;
this.queryPort = queryPort;
this.forwardingSecret = forwardingSecret;
}
public boolean validate() {
@@ -68,9 +83,15 @@ public class VelocityConfiguration {
logger.info("Proxy is running in offline mode!");
}
switch (ipForwardingMode) {
switch (playerInfoForwardingMode) {
case NONE:
logger.info("IP forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
logger.info("Player info forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
break;
case MODERN:
if (forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set.");
valid = false;
}
break;
}
@@ -109,23 +130,51 @@ public class VelocityConfiguration {
if (compressionLevel < -1 || compressionLevel > 9) {
logger.error("Invalid compression level {}", compressionLevel);
valid = false;
} else if (compressionLevel == 0) {
logger.warn("ALL packets going through the proxy are going to be uncompressed. This will increase bandwidth usage.");
}
if (compressionThreshold < -1) {
logger.error("Invalid compression threshold {}", compressionLevel);
valid = false;
} else if (compressionThreshold == 0) {
logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance.");
}
if (loginRatelimit < 0) {
logger.error("Invalid login ratelimit {}", loginRatelimit);
valid = false;
}
loadFavicon();
return valid;
}
private void loadFavicon() {
Path faviconPath = Paths.get("server-icon.png");
if (Files.exists(faviconPath)) {
try {
this.favicon = Favicon.create(faviconPath);
} catch (Exception e) {
logger.info("Unable to load your server-icon.png, continuing without it.", e);
}
}
}
public InetSocketAddress getBind() {
return AddressUtil.parseAddress(bind);
}
public boolean isQueryEnabled() {
return queryEnabled;
}
public int getQueryPort() {
return queryPort;
}
public String getMotd() {
return motd;
}
@@ -149,8 +198,8 @@ public class VelocityConfiguration {
return onlineMode;
}
public IPForwardingMode getIpForwardingMode() {
return ipForwardingMode;
public PlayerInfoForwarding getPlayerInfoForwardingMode() {
return playerInfoForwardingMode;
}
public Map<String, String> getServers() {
@@ -169,6 +218,18 @@ public class VelocityConfiguration {
return compressionLevel;
}
public int getLoginRatelimit() {
return loginRatelimit;
}
public Favicon getFavicon() {
return favicon;
}
public byte[] getForwardingSecret() {
return forwardingSecret;
}
@Override
public String toString() {
return "VelocityConfiguration{" +
@@ -176,12 +237,17 @@ public class VelocityConfiguration {
", motd='" + motd + '\'' +
", showMaxPlayers=" + showMaxPlayers +
", onlineMode=" + onlineMode +
", ipForwardingMode=" + ipForwardingMode +
", playerInfoForwardingMode=" + playerInfoForwardingMode +
", servers=" + servers +
", attemptConnectionOrder=" + attemptConnectionOrder +
", compressionThreshold=" + compressionThreshold +
", compressionLevel=" + compressionLevel +
", loginRatelimit=" + loginRatelimit +
", queryEnabled=" + queryEnabled +
", queryPort=" + queryPort +
", motdAsComponent=" + motdAsComponent +
", favicon=" + favicon +
", forwardingSecret=" + ByteBufUtil.hexDump(forwardingSecret) +
'}';
}
@@ -200,16 +266,23 @@ public class VelocityConfiguration {
}
}
byte[] forwardingSecret = toml.getString("player-info-forwarding-secret", "5up3r53cr3t")
.getBytes(StandardCharsets.UTF_8);
return new VelocityConfiguration(
toml.getString("bind"),
toml.getString("motd"),
toml.getLong("show-max-players").intValue(),
toml.getBoolean("online-mode"),
IPForwardingMode.valueOf(toml.getString("ip-forwarding").toUpperCase()),
toml.getString("bind", "0.0.0.0:25577"),
toml.getString("motd", "&3A Velocity Server"),
toml.getLong("show-max-players", 500L).intValue(),
toml.getBoolean("online-mode", true),
PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding", "MODERN").toUpperCase()),
ImmutableMap.copyOf(servers),
toml.getTable("servers").getList("try"),
toml.getTable("advanced").getLong("compression-threshold", 1024L).intValue(),
toml.getTable("advanced").getLong("compression-level", -1L).intValue());
toml.getTable("advanced").getLong("compression-level", -1L).intValue(),
toml.getTable("advanced").getLong("login-ratelimit", 3000L).intValue(),
toml.getTable("query").getBoolean("enabled", false),
toml.getTable("query").getLong("port", 25577L).intValue(),
forwardingSecret);
}
}
}

View File

@@ -5,11 +5,12 @@ import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.natives.encryption.VelocityCipherFactory;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.protocol.PacketWrapper;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.natives.encryption.JavaVelocityCipher;
import com.velocitypowered.natives.encryption.VelocityCipher;
import com.velocitypowered.proxy.protocol.netty.*;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
@@ -23,14 +24,14 @@ import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import static com.velocitypowered.network.Connections.CIPHER_DECODER;
import static com.velocitypowered.network.Connections.CIPHER_ENCODER;
import static com.velocitypowered.network.Connections.COMPRESSION_DECODER;
import static com.velocitypowered.network.Connections.COMPRESSION_ENCODER;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
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;
/**
* A utility class to make working with the pipeline a little less painful and transparently handles certain Minecraft
@@ -40,7 +41,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
private final Channel channel;
private boolean closed;
private StateRegistry state;
private MinecraftSessionHandler sessionHandler;
private int protocolVersion;
@@ -48,7 +48,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public MinecraftConnection(Channel channel) {
this.channel = channel;
this.closed = false;
this.state = StateRegistry.HANDSHAKE;
}
@@ -72,24 +71,17 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (association != null) {
logger.info("{} has disconnected", association);
}
teardown();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof PacketWrapper) {
PacketWrapper pw = (PacketWrapper) msg;
if (msg instanceof MinecraftPacket) {
sessionHandler.handle((MinecraftPacket) msg);
} else if (msg instanceof ByteBuf) {
try {
if (sessionHandler != null) {
if (pw.getPacket() == null) {
sessionHandler.handleUnknown(pw.getBuffer());
} else {
sessionHandler.handle(pw.getPacket());
}
}
sessionHandler.handleUnknown((ByteBuf) msg);
} finally {
ReferenceCountUtil.release(pw.getBuffer());
ReferenceCountUtil.release(msg);
}
}
}
@@ -107,40 +99,38 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
logger.error("{} encountered an exception", ctx.channel().remoteAddress(), cause);
}
closed = true;
ctx.close();
}
}
public void write(Object msg) {
ensureOpen();
channel.writeAndFlush(msg, channel.voidPromise());
if (channel.isActive()) {
channel.writeAndFlush(msg, channel.voidPromise());
}
}
public void delayedWrite(Object msg) {
ensureOpen();
channel.write(msg, channel.voidPromise());
if (channel.isActive()) {
channel.write(msg, channel.voidPromise());
}
}
public void flush() {
ensureOpen();
channel.flush();
if (channel.isActive()) {
channel.flush();
}
}
public void closeWith(Object msg) {
ensureOpen();
teardown();
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
if (channel.isActive()) {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
public void close() {
ensureOpen();
teardown();
channel.close();
}
public void teardown() {
closed = true;
if (channel.isActive()) {
channel.close();
}
}
public Channel getChannel() {
@@ -148,7 +138,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
public boolean isClosed() {
return closed;
return !channel.isActive();
}
public StateRegistry getState() {
@@ -167,8 +157,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion);
this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(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);
}
}
public MinecraftSessionHandler getSessionHandler() {
@@ -184,10 +180,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
private void ensureOpen() {
Preconditions.checkState(!closed, "Connection is closed.");
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);
@@ -204,6 +202,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
ensureOpen();
SecretKey key = new SecretKeySpec(secret, "AES");
VelocityCipherFactory factory = Natives.cipher.get();

View File

@@ -4,7 +4,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import io.netty.buffer.ByteBuf;
public interface MinecraftSessionHandler {
void handle(MinecraftPacket packet) throws Exception;
void handle(MinecraftPacket packet);
default void handleUnknown(ByteBuf buf) {
// No-op: we'll release the buffer later.

View File

@@ -1,5 +1,9 @@
package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.proxy.messages.ChannelSide;
import com.velocitypowered.api.proxy.messages.MessageHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
@@ -7,31 +11,40 @@ import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
private final ServerConnection connection;
private final VelocityServerConnection connection;
public BackendPlaySessionHandler(ServerConnection connection) {
public BackendPlaySessionHandler(VelocityServerConnection connection) {
this.connection = connection;
}
@Override
public void activated() {
VelocityServer.getServer().getEventManager().fireAndForget(new ServerConnectedEvent(connection.getPlayer(),
connection.getServerInfo()));
}
@Override
public void handle(MinecraftPacket packet) {
if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors.
connection.getMinecraftConnection().close();
return;
}
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
if (packet instanceof KeepAlive) {
// Forward onto the server
connection.getMinecraftConnection().write(packet);
// Forward onto the player
playerHandler.setLastPing(((KeepAlive) packet).getRandomId());
connection.getPlayer().getConnection().write(packet);
} else if (packet instanceof Disconnect) {
Disconnect original = (Disconnect) packet;
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), original);
connection.getPlayer().handleConnectionException(connection.getServerInfo(), original);
} else if (packet instanceof JoinGame) {
playerHandler.handleBackendJoinGame((JoinGame) packet);
} else if (packet instanceof Respawn) {
// Record the dimension switch, and then forward the packet on.
playerHandler.setCurrentDimension(((Respawn) packet).getDimension());
connection.getProxyPlayer().getConnection().write(packet);
} else if (packet instanceof BossBar) {
BossBar bossBar = (BossBar) packet;
switch (bossBar.getAction()) {
@@ -42,7 +55,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
playerHandler.getServerBossBars().remove(bossBar.getUuid());
break;
}
connection.getProxyPlayer().getConnection().write(packet);
connection.getPlayer().getConnection().write(packet);
} else if (packet instanceof PluginMessage) {
PluginMessage pm = (PluginMessage) packet;
if (!canForwardPluginMessage(pm)) {
@@ -50,45 +63,52 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
}
if (PluginMessageUtil.isMCBrand(pm)) {
connection.getProxyPlayer().getConnection().write(PluginMessageUtil.rewriteMCBrand(pm));
connection.getPlayer().getConnection().write(PluginMessageUtil.rewriteMCBrand(pm));
return;
}
connection.getProxyPlayer().getConnection().write(pm);
MessageHandler.ForwardStatus status = VelocityServer.getServer().getChannelRegistrar().handlePluginMessage(
connection, ChannelSide.FROM_SERVER, pm);
if (status == MessageHandler.ForwardStatus.FORWARD) {
connection.getPlayer().getConnection().write(pm);
}
} else {
// Just forward the packet on. We don't have anything to handle at this time.
if (packet instanceof ScoreboardTeam ||
packet instanceof ScoreboardObjective ||
packet instanceof ScoreboardSetScore ||
packet instanceof ScoreboardDisplay) {
playerHandler.handleServerScoreboardPacket(packet);
}
connection.getProxyPlayer().getConnection().write(packet);
connection.getPlayer().getConnection().write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors.
connection.getMinecraftConnection().close();
return;
}
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
ByteBuf remapped = playerHandler.getIdRemapper().remap(buf, ProtocolConstants.Direction.CLIENTBOUND);
connection.getProxyPlayer().getConnection().write(remapped);
connection.getPlayer().getConnection().write(remapped);
}
@Override
public void exception(Throwable throwable) {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), throwable);
connection.getPlayer().handleConnectionException(connection.getServerInfo(), throwable);
}
private boolean canForwardPluginMessage(PluginMessage message) {
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
if (connection.getMinecraftConnection().getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) {
return message.getChannel().startsWith("MC|") ||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel());
playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
VelocityServer.getServer().getChannelRegistrar().registered(message.getChannel());
} else {
return message.getChannel().startsWith("minecraft:") ||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel());
playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
VelocityServer.getServer().getChannelRegistrar().registered(message.getChannel());
}
}
}

View File

@@ -2,11 +2,12 @@ package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.IPForwardingMode;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
@@ -14,49 +15,40 @@ import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelPipeline;
import net.kyori.text.TextComponent;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CompletableFuture;
public class LoginSessionHandler implements MinecraftSessionHandler {
private final ServerConnection connection;
private ScheduledFuture<?> forwardingCheckTask;
private final VelocityServerConnection connection;
private boolean informationForwarded;
public LoginSessionHandler(ServerConnection connection) {
public LoginSessionHandler(VelocityServerConnection connection) {
this.connection = connection;
}
@Override
public void activated() {
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN) {
forwardingCheckTask = connection.getMinecraftConnection().getChannel().eventLoop().schedule(() -> {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(),
TextComponent.of("Your server did not send the forwarding request in time. Is it set up correctly?"));
}, 1, TimeUnit.SECONDS);
}
}
@Override
public void handle(MinecraftPacket packet) {
if (packet instanceof EncryptionRequest) {
throw new IllegalStateException("Backend server is online-mode!");
} else if (packet instanceof LoginPluginMessage) {
LoginPluginMessage message = (LoginPluginMessage) packet;
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN &&
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN &&
message.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(true);
response.setId(message.getId());
response.setData(createForwardingData(connection.getProxyPlayer().getRemoteAddress().getHostString(),
connection.getProxyPlayer().getProfile()));
response.setData(createForwardingData(configuration.getForwardingSecret(),
connection.getPlayer().getRemoteAddress().getHostString(),
connection.getPlayer().getProfile()));
connection.getMinecraftConnection().write(response);
cancelForwardingCheck();
ServerLogin login = new ServerLogin();
login.setUsername(connection.getProxyPlayer().getUsername());
connection.getMinecraftConnection().write(login);
informationForwarded = true;
} else {
// Don't understand
LoginPluginResponse response = new LoginPluginResponse();
@@ -67,78 +59,92 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
} else if (packet instanceof Disconnect) {
Disconnect disconnect = (Disconnect) packet;
connection.disconnect();
// Do we have an outstanding notification? If so, fulfill it.
doNotify(ConnectionRequestResults.forDisconnect(disconnect));
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), disconnect);
connection.disconnect();
} else if (packet instanceof SetCompression) {
SetCompression sc = (SetCompression) packet;
connection.getMinecraftConnection().setCompressionThreshold(sc.getThreshold());
} else if (packet instanceof ServerLoginSuccess) {
if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN &&
!informationForwarded) {
doNotify(ConnectionRequestResults.forDisconnect(
TextComponent.of("Your server did not send a forwarding request to the proxy. Is it set up correctly?")));
connection.disconnect();
return;
}
// The player has been logged on to the backend server.
connection.getMinecraftConnection().setState(StateRegistry.PLAY);
ServerConnection existingConnection = connection.getProxyPlayer().getConnectedServer();
VelocityServerConnection existingConnection = connection.getPlayer().getConnectedServer();
if (existingConnection == null) {
// Strap on the play session handler
connection.getProxyPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(connection.getProxyPlayer()));
connection.getPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(connection.getPlayer()));
} else {
// The previous server connection should become obsolete.
existingConnection.disconnect();
}
// Do we have an outstanding notification? If so, fulfill it.
doNotify(ConnectionRequestResults.SUCCESSFUL);
connection.getMinecraftConnection().setSessionHandler(new BackendPlaySessionHandler(connection));
connection.getProxyPlayer().setConnectedServer(connection);
connection.getPlayer().setConnectedServer(connection);
}
}
@Override
public void deactivated() {
cancelForwardingCheck();
}
@Override
public void exception(Throwable throwable) {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), throwable);
CompletableFuture<ConnectionRequestBuilder.Result> future = connection.getMinecraftConnection().getChannel()
.attr(VelocityServerConnection.CONNECTION_NOTIFIER).getAndSet(null);
if (future != null) {
future.completeExceptionally(throwable);
}
}
private void doNotify(ConnectionRequestBuilder.Result result) {
ChannelPipeline pipeline = connection.getMinecraftConnection().getChannel().pipeline();
ServerConnection.ConnectionNotifier n = pipeline.get(ServerConnection.ConnectionNotifier.class);
if (n != null) {
n.getResult().complete(result);
pipeline.remove(ServerConnection.ConnectionNotifier.class);
CompletableFuture<ConnectionRequestBuilder.Result> future = connection.getMinecraftConnection().getChannel()
.attr(VelocityServerConnection.CONNECTION_NOTIFIER).getAndSet(null);
if (future != null) {
future.complete(result);
}
}
private void cancelForwardingCheck() {
if (forwardingCheckTask != null) {
forwardingCheckTask.cancel(false);
forwardingCheckTask = null;
}
}
private static ByteBuf createForwardingData(String address, GameProfile profile) {
ByteBuf buf = Unpooled.buffer();
ProtocolUtils.writeString(buf, address);
ProtocolUtils.writeUuid(buf, profile.idAsUuid());
ProtocolUtils.writeString(buf, profile.getName());
ProtocolUtils.writeVarInt(buf, profile.getProperties().size());
for (GameProfile.Property property : profile.getProperties()) {
ProtocolUtils.writeString(buf, property.getName());
ProtocolUtils.writeString(buf, property.getValue());
String signature = property.getSignature();
if (signature != null) {
buf.writeBoolean(true);
ProtocolUtils.writeString(buf, signature);
} else {
buf.writeBoolean(false);
static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
ByteBuf dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer();
try {
ProtocolUtils.writeString(dataToForward, address);
ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid());
ProtocolUtils.writeString(dataToForward, profile.getName());
ProtocolUtils.writeVarInt(dataToForward, profile.getProperties().size());
for (GameProfile.Property property : profile.getProperties()) {
ProtocolUtils.writeString(dataToForward, property.getName());
ProtocolUtils.writeString(dataToForward, property.getValue());
String signature = property.getSignature();
if (signature != null) {
dataToForward.writeBoolean(true);
ProtocolUtils.writeString(dataToForward, signature);
} else {
dataToForward.writeBoolean(false);
}
}
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();
}
return buf;
}
}

View File

@@ -1,44 +1,49 @@
package com.velocitypowered.proxy.connection.backend;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.config.IPForwardingMode;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import io.netty.channel.*;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.util.AttributeKey;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.network.Connections.HANDLER;
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.network.Connections.READ_TIMEOUT;
import static com.velocitypowered.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
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 static com.velocitypowered.proxy.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
public class ServerConnection implements MinecraftConnectionAssociation {
static final String CONNECTION_NOTIFIER = "connection-notifier";
public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection {
static final AttributeKey<CompletableFuture<ConnectionRequestBuilder.Result>> CONNECTION_NOTIFIER =
AttributeKey.newInstance("connection-notification-result");
private final ServerInfo serverInfo;
private final ConnectedPlayer proxyPlayer;
private final VelocityServer server;
private MinecraftConnection minecraftConnection;
public ServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
public VelocityServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
this.serverInfo = target;
this.proxyPlayer = proxyPlayer;
this.server = server;
@@ -55,12 +60,12 @@ public class ServerConnection implements MinecraftConnectionAssociation {
.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))
.addLast(CONNECTION_NOTIFIER, new ConnectionNotifier(result));
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
ch.attr(CONNECTION_NOTIFIER).set(result);
MinecraftConnection connection = new MinecraftConnection(ch);
connection.setState(StateRegistry.HANDSHAKE);
connection.setAssociation(ServerConnection.this);
connection.setAssociation(VelocityServerConnection.this);
ch.pipeline().addLast(HANDLER, connection);
}
})
@@ -72,7 +77,7 @@ public class ServerConnection implements MinecraftConnectionAssociation {
minecraftConnection = future.channel().pipeline().get(MinecraftConnection.class);
// Kick off the connection process
minecraftConnection.setSessionHandler(new LoginSessionHandler(ServerConnection.this));
minecraftConnection.setSessionHandler(new LoginSessionHandler(VelocityServerConnection.this));
startHandshake();
} else {
result.completeExceptionally(future.cause());
@@ -93,11 +98,13 @@ public class ServerConnection implements MinecraftConnectionAssociation {
}
private void startHandshake() {
PlayerInfoForwarding forwardingMode = VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode();
// Initiate a handshake.
Handshake handshake = new Handshake();
handshake.setNextStatus(StateRegistry.LOGIN_ID);
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.LEGACY) {
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createBungeeForwardingAddress());
} else {
handshake.setServerAddress(serverInfo.getAddress().getHostString());
@@ -109,17 +116,9 @@ public class ServerConnection implements MinecraftConnectionAssociation {
minecraftConnection.setProtocolVersion(protocolVersion);
minecraftConnection.setState(StateRegistry.LOGIN);
// Send the server login packet for <=1.12.2 and for 1.13+ servers not using "modern" forwarding.
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ||
VelocityServer.getServer().getConfiguration().getIpForwardingMode() != IPForwardingMode.MODERN) {
ServerLogin login = new ServerLogin();
login.setUsername(proxyPlayer.getUsername());
minecraftConnection.write(login);
}
}
public ConnectedPlayer getProxyPlayer() {
return proxyPlayer;
ServerLogin login = new ServerLogin();
login.setUsername(proxyPlayer.getUsername());
minecraftConnection.write(login);
}
public MinecraftConnection getMinecraftConnection() {
@@ -130,6 +129,11 @@ public class ServerConnection implements MinecraftConnectionAssociation {
return serverInfo;
}
@Override
public ConnectedPlayer getPlayer() {
return proxyPlayer;
}
public void disconnect() {
minecraftConnection.close();
minecraftConnection = null;
@@ -140,24 +144,13 @@ public class ServerConnection implements MinecraftConnectionAssociation {
return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + serverInfo.getName();
}
static class ConnectionNotifier extends ChannelInboundHandlerAdapter {
private final CompletableFuture<ConnectionRequestBuilder.Result> result;
public ConnectionNotifier(CompletableFuture<ConnectionRequestBuilder.Result> result) {
this.result = result;
}
public CompletableFuture<ConnectionRequestBuilder.Result> getResult() {
return result;
}
public void onComplete() {
result.complete(ConnectionRequestResults.SUCCESSFUL);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
result.completeExceptionally(cause);
}
@Override
public void sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
Preconditions.checkNotNull(identifier, "identifier");
Preconditions.checkNotNull(data, "data");
PluginMessage message = new PluginMessage();
message.setChannel(identifier.getId());
message.setData(data);
minecraftConnection.write(message);
}
}

View File

@@ -1,47 +1,41 @@
package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.proxy.messages.ChannelSide;
import com.velocitypowered.api.proxy.messages.MessageHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.data.scoreboard.Objective;
import com.velocitypowered.proxy.data.scoreboard.Score;
import com.velocitypowered.proxy.data.scoreboard.Scoreboard;
import com.velocitypowered.proxy.data.scoreboard.Team;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.remap.EntityIdRemapper;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import com.velocitypowered.proxy.util.ThrowableUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.EventLoop;
import io.netty.util.ReferenceCountUtil;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* 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 = 128;
private final ConnectedPlayer player;
private ScheduledFuture<?> pingTask;
private long lastPing = -1;
private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>();
private final Set<String> clientPluginMsgChannels = new HashSet<>();
private int currentDimension;
private Scoreboard serverScoreboard = new Scoreboard();
private EntityIdRemapper idRemapper;
public ClientPlaySessionHandler(ConnectedPlayer player) {
@@ -50,16 +44,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void activated() {
EventLoop loop = player.getConnection().getChannel().eventLoop();
pingTask = loop.scheduleAtFixedRate(this::ping, 5, 15, TimeUnit.SECONDS);
}
private void ping() {
long randomId = ThreadLocalRandom.current().nextInt();
lastPing = randomId;
KeepAlive keepAlive = new KeepAlive();
keepAlive.setRandomId(randomId);
player.getConnection().write(keepAlive);
PluginMessage message;
if (player.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
message = PluginMessageUtil.constructChannelsPacket("minecraft:register", VelocityServer.getServer().getChannelRegistrar().getModernChannelIds());
} else {
message = PluginMessageUtil.constructChannelsPacket("REGISTER", VelocityServer.getServer().getChannelRegistrar().getLegacyChannelIds());
}
player.getConnection().write(message);
}
@Override
@@ -67,11 +58,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (packet instanceof KeepAlive) {
KeepAlive keepAlive = (KeepAlive) packet;
if (keepAlive.getRandomId() != lastPing) {
throw new IllegalStateException("Client sent invalid keepAlive; expected " + lastPing + ", got " + keepAlive.getRandomId());
// The last keep alive we got was probably from a different server. Let's ignore it, and hope the next
// ping is alright.
return;
}
// Do not forward the packet to the player's server, because we handle pings for all servers already.
return;
}
if (packet instanceof ClientSettings) {
@@ -80,10 +70,49 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
if (packet instanceof Chat) {
// Try to handle any commands on the proxy. If that fails, send it onto the client.
Chat chat = (Chat) packet;
if (chat.getMessage().equals("/connect")) {
ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25566));
player.createConnectionRequest(info).fireAndForget();
String msg = ((Chat) packet).getMessage();
if (msg.startsWith("/")) {
try {
if (!VelocityServer.getServer().getCommandManager().execute(player, msg.substring(1))) {
player.getConnectedServer().getMinecraftConnection().write(chat);
}
} catch (Exception e) {
logger.info("Exception occurred while running command for {}", player.getProfile().getName(), e);
player.sendMessage(TextComponent.of("An error occurred while running this command.", TextColor.RED));
return;
}
} else {
player.getConnectedServer().getMinecraftConnection().write(chat);
}
return;
}
if (packet instanceof TabCompleteRequest) {
TabCompleteRequest req = (TabCompleteRequest) packet;
int lastSpace = req.getCommand().indexOf(' ');
if (!req.isAssumeCommand() && lastSpace != -1) {
String command = req.getCommand().substring(1);
try {
Optional<List<String>> offers = VelocityServer.getServer().getCommandManager().offerSuggestions(player, command);
if (offers.isPresent()) {
TabCompleteResponse response = new TabCompleteResponse();
response.setTransactionId(req.getTransactionId());
response.setStart(lastSpace);
response.setLength(req.getCommand().length() - lastSpace);
for (String s : offers.get()) {
response.getOffers().add(new TabCompleteResponse.Offer(s, null));
}
player.getConnection().write(response);
} else {
player.getConnectedServer().getMinecraftConnection().write(packet);
}
} catch (Exception e) {
logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", e);
}
return;
}
}
@@ -106,11 +135,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void disconnected() {
player.teardown();
if (pingTask != null && !pingTask.isCancelled()) {
pingTask.cancel(false);
pingTask = null;
}
VelocityServer.getServer().getEventManager().fireAndForget(new DisconnectEvent(player));
}
@Override
@@ -123,21 +148,22 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
public void handleBackendJoinGame(JoinGame joinGame) {
lastPing = Long.MIN_VALUE; // reset last ping
if (!spawned) {
// nothing special to do here
spawned = true;
currentDimension = joinGame.getDimension();
player.getConnection().delayedWrite(joinGame);
idRemapper = EntityIdRemapper.getMapper(joinGame.getEntityId(), player.getConnection().getProtocolVersion());
} else {
// In order to handle switching to another server we will need send three packets:
// Ah, this is the meat and potatoes of the whole venture!
//
// 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
//
// We can't simply ignore the packet with the different dimension. If you try to be smart about it it doesn't
// work.
// 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
@@ -147,13 +173,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
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()));
currentDimension = joinGame.getDimension();
}
// Resend client settings packet to remote server if we have it, this preserves client settings across
// transitions.
if (player.getClientSettings() != null) {
player.getConnectedServer().getMinecraftConnection().delayedWrite(player.getClientSettings());
}
// Remove old boss bars.
@@ -165,15 +184,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
serverBossBars.clear();
// Remove scoreboard junk.
clearServerScoreboard();
// Tell the server about this client's plugin messages. Velocity will forward them on to the client.
if (!clientPluginMsgChannels.isEmpty()) {
Collection<String> toRegister = new HashSet<>(clientPluginMsgChannels);
if (player.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
toRegister.addAll(VelocityServer.getServer().getChannelRegistrar().getModernChannelIds());
} else {
toRegister.addAll(VelocityServer.getServer().getChannelRegistrar().getLegacyChannelIds());
}
if (!toRegister.isEmpty()) {
String channel = player.getConnection().getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13 ?
"minecraft:register" : "REGISTER";
player.getConnectedServer().getMinecraftConnection().delayedWrite(
PluginMessageUtil.constructChannelsPacket(channel, clientPluginMsgChannels));
player.getConnectedServer().getMinecraftConnection().delayedWrite(PluginMessageUtil.constructChannelsPacket(
channel, toRegister));
}
// Flush everything
@@ -181,10 +203,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
player.getConnectedServer().getMinecraftConnection().flush();
}
public void setCurrentDimension(int currentDimension) {
this.currentDimension = currentDimension;
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
@@ -204,7 +222,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
if (actuallyRegistered.size() > 0) {
logger.info("Rewritten register packet: {}", actuallyRegistered);
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered);
player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket);
}
@@ -222,88 +239,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return;
}
// We're going to forward on the original packet.
player.getConnectedServer().getMinecraftConnection().write(packet);
}
public void handleServerScoreboardPacket(MinecraftPacket packet) {
if (packet instanceof ScoreboardDisplay) {
ScoreboardDisplay sd = (ScoreboardDisplay) packet;
serverScoreboard.setPosition(sd.getPosition());
serverScoreboard.setDisplayName(sd.getDisplayName());
MessageHandler.ForwardStatus status = VelocityServer.getServer().getChannelRegistrar().handlePluginMessage(
player, ChannelSide.FROM_CLIENT, packet);
if (status == MessageHandler.ForwardStatus.FORWARD) {
// We're going to forward on the original packet.
player.getConnectedServer().getMinecraftConnection().write(packet);
}
if (packet instanceof ScoreboardObjective) {
ScoreboardObjective so = (ScoreboardObjective) packet;
switch (so.getMode()) {
case ScoreboardObjective.ADD:
Objective o = new Objective(so.getId());
o.setDisplayName(so.getDisplayName());
o.setType(so.getType());
serverScoreboard.getObjectives().put(so.getId(), o);
break;
case ScoreboardObjective.REMOVE:
serverScoreboard.getObjectives().remove(so.getId());
break;
}
}
if (packet instanceof ScoreboardSetScore) {
ScoreboardSetScore sss = (ScoreboardSetScore) packet;
Objective objective = serverScoreboard.getObjectives().get(sss.getObjective());
if (objective == null) {
return;
}
switch (sss.getAction()) {
case ScoreboardSetScore.CHANGE:
Score score = new Score(sss.getEntity(), sss.getValue());
objective.getScores().put(sss.getEntity(), score);
break;
case ScoreboardSetScore.REMOVE:
objective.getScores().remove(sss.getEntity());
break;
}
}
if (packet instanceof ScoreboardTeam) {
ScoreboardTeam st = (ScoreboardTeam) packet;
switch (st.getMode()) {
case ScoreboardTeam.ADD:
// TODO: Preserve other team information? We might not need to...
Team team = new Team(st.getId());
serverScoreboard.getTeams().put(st.getId(), team);
break;
case ScoreboardTeam.REMOVE:
serverScoreboard.getTeams().remove(st.getId());
break;
}
}
}
private void clearServerScoreboard() {
for (Objective objective : serverScoreboard.getObjectives().values()) {
for (Score score : objective.getScores().values()) {
ScoreboardSetScore sss = new ScoreboardSetScore();
sss.setObjective(objective.getId());
sss.setAction(ScoreboardSetScore.REMOVE);
sss.setEntity(score.getTarget());
player.getConnection().delayedWrite(sss);
}
ScoreboardObjective so = new ScoreboardObjective();
so.setId(objective.getId());
so.setMode(ScoreboardObjective.REMOVE);
player.getConnection().delayedWrite(so);
}
for (Team team : serverScoreboard.getTeams().values()) {
ScoreboardTeam st = new ScoreboardTeam();
st.setId(team.getId());
st.setMode(ScoreboardTeam.REMOVE);
player.getConnection().delayedWrite(st);
}
serverScoreboard = new Scoreboard();
}
public Set<String> getClientPluginMsgChannels() {
@@ -313,4 +254,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
public EntityIdRemapper getIdRemapper() {
return idRemapper;
}
public void setLastPing(long lastPing) {
this.lastPing = lastPing;
}
}

View File

@@ -2,21 +2,29 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.gson.JsonObject;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.permission.PermissionFunction;
import com.velocitypowered.api.permission.PermissionProvider;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.util.MessagePosition;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.packet.Chat;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.util.ThrowableUtils;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
@@ -25,8 +33,8 @@ import net.kyori.text.serializer.ComponentSerializers;
import net.kyori.text.serializer.PlainComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import javax.annotation.Nonnull;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Optional;
@@ -35,19 +43,23 @@ import java.util.concurrent.CompletableFuture;
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private static final PlainComponentSerializer PASS_THRU_TRANSLATE = new PlainComponentSerializer((c) -> "", TranslatableComponent::key);
public static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class);
private final GameProfile profile;
private final MinecraftConnection connection;
private final InetSocketAddress virtualHost;
private final GameProfile profile;
private PermissionFunction permissionFunction = null;
private int tryIndex = 0;
private ServerConnection connectedServer;
private VelocityServerConnection connectedServer;
private ClientSettings clientSettings;
private ServerConnection connectionInFlight;
private VelocityServerConnection connectionInFlight;
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection) {
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection, InetSocketAddress virtualHost) {
this.profile = profile;
this.connection = connection;
this.virtualHost = virtualHost;
}
@Override
@@ -61,8 +73,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
@Override
public Optional<ServerInfo> getCurrentServer() {
return connectedServer != null ? Optional.of(connectedServer.getServerInfo()) : Optional.empty();
public Optional<ServerConnection> getCurrentServer() {
return Optional.ofNullable(connectedServer);
}
public GameProfile getProfile() {
@@ -78,13 +90,27 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.ofNullable(virtualHost);
}
public void setPermissionFunction(PermissionFunction permissionFunction) {
this.permissionFunction = permissionFunction;
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
}
@Override
public void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position) {
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
@Override
public void sendMessage(@NonNull Component component, @NonNull MessagePosition position) {
Preconditions.checkNotNull(component, "component");
Preconditions.checkNotNull(position, "position");
@@ -107,11 +133,28 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
@Override
public ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info) {
public ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info) {
return new ConnectionRequestBuilderImpl(info);
}
public ServerConnection getConnectedServer() {
@Override
public void setHeaderAndFooter(@NonNull Component header, @NonNull 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 disconnect(Component reason) {
connection.closeWith(Disconnect.create(reason));
}
public VelocityServerConnection getConnectedServer() {
return connectedServer;
}
@@ -127,7 +170,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
String error = ThrowableUtils.briefDescription(throwable);
String userMessage;
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
logger.error("{}: exception occurred in connection to {}", this, info.getName(), throwable);
userMessage = "Exception in server " + info.getName();
} else {
logger.error("{}: unable to connect to server {}", this, info.getName(), throwable);
@@ -153,9 +195,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
public void handleConnectionException(ServerInfo info, Component disconnectReason) {
connectionInFlight = null;
if (connectedServer == null || connectedServer.getServerInfo().equals(info)) {
// The player isn't yet connected to a server or they are already connected to the server
// they're disconnected from.
if (connectedServer == null) {
// The player isn't yet connected to a server.
Optional<ServerInfo> nextServer = getNextServerToTry();
if (nextServer.isPresent()) {
createConnectionRequest(nextServer.get()).fireAndForget();
} else {
connection.closeWith(Disconnect.create(disconnectReason));
}
} else if (connectedServer.getServerInfo().equals(info)) {
// Already connected to the server being disconnected from.
// TODO: ServerKickEvent
connection.closeWith(Disconnect.create(disconnectReason));
} else {
connection.write(Chat.create(disconnectReason));
@@ -187,11 +237,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
// Otherwise, initiate the connection.
ServerConnection connection = new ServerConnection(request.getServer(), this, VelocityServer.getServer());
return connection.connect();
ServerPreConnectEvent event = new ServerPreConnectEvent(this, ServerPreConnectEvent.ServerResult.allowed(request.getServer()));
return VelocityServer.getServer().getEventManager().fire(event)
.thenCompose((newEvent) -> {
if (!newEvent.getResult().isAllowed()) {
return CompletableFuture.completedFuture(
ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED)
);
}
return new VelocityServerConnection(newEvent.getResult().getInfo().get(), this, VelocityServer.getServer()).connect();
});
}
public void setConnectedServer(ServerConnection serverConnection) {
public void setConnectedServer(VelocityServerConnection serverConnection) {
if (this.connectedServer != null && !serverConnection.getServerInfo().equals(connectedServer.getServerInfo())) {
this.tryIndex = 0;
}
@@ -209,6 +268,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (connectedServer != null) {
connectedServer.disconnect();
}
VelocityServer.getServer().unregisterConnection(this);
}
@Override
@@ -216,10 +276,25 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return "[connected player] " + getProfile().getName() + " (" + getRemoteAddress() + ")";
}
@Override
public boolean hasPermission(String permission) {
return permissionFunction.getPermissionSetting(permission).asBoolean();
}
@Override
public void sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
Preconditions.checkNotNull(identifier, "identifier");
Preconditions.checkNotNull(data, "data");
PluginMessage message = new PluginMessage();
message.setChannel(identifier.getId());
message.setData(data);
connection.write(message);
}
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final ServerInfo info;
public ConnectionRequestBuilderImpl(ServerInfo info) {
ConnectionRequestBuilderImpl(ServerInfo info) {
this.info = Preconditions.checkNotNull(info, "info");
}
@@ -236,7 +311,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
@Override
public void fireAndForget() {
connect()
.whenComplete((status, throwable) -> {
.whenCompleteAsync((status, throwable) -> {
if (throwable != null) {
handleConnectionException(info, throwable);
return;
@@ -256,7 +331,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
handleConnectionException(info, Disconnect.create(status.getReason().orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR)));
break;
}
});
}, connection.getChannel().eventLoop());
}
}
}

View File

@@ -1,14 +1,28 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.event.connection.ConnectionHandshakeEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import com.velocitypowered.proxy.protocol.packet.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
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;
@@ -19,16 +33,24 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
@Override
public void handle(MinecraftPacket packet) {
if (packet instanceof LegacyPing || packet instanceof LegacyHandshake) {
connection.setProtocolVersion(ProtocolConstants.LEGACY);
handleLegacy(packet);
return;
}
if (!(packet instanceof Handshake)) {
throw new IllegalArgumentException("Did not expect packet " + packet.getClass().getName());
}
InitialInboundConnection ic = new InitialInboundConnection(connection, (Handshake) packet);
Handshake handshake = (Handshake) packet;
switch (handshake.getNextStatus()) {
case StateRegistry.STATUS_ID:
connection.setState(StateRegistry.STATUS);
connection.setProtocolVersion(handshake.getProtocolVersion());
connection.setSessionHandler(new StatusSessionHandler(connection));
connection.setSessionHandler(new StatusSessionHandler(connection, ic));
break;
case StateRegistry.LOGIN_ID:
connection.setState(StateRegistry.LOGIN);
@@ -37,12 +59,70 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
return;
} else {
connection.setSessionHandler(new LoginSessionHandler(connection));
InetAddress address = ((InetSocketAddress) connection.getChannel().remoteAddress()).getAddress();
if (!VelocityServer.getServer().getIpAttemptLimiter().attempt(address)) {
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
return;
}
VelocityServer.getServer().getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
connection.setSessionHandler(new LoginSessionHandler(connection, ic));
}
break;
default:
throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus());
}
}
@Override
public void handleUnknown(ByteBuf buf) {
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
}
private void handleLegacy(MinecraftPacket packet) {
if (packet instanceof LegacyPing) {
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
ServerPing ping = new ServerPing(
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
null
);
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);
VelocityServer.getServer().getEventManager().fire(event)
.thenRunAsync(() -> {
// The disconnect packet is the same as the server response one.
connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(event.getPing())));
}, connection.getChannel().eventLoop());
} else if (packet instanceof LegacyHandshake) {
connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED)));
}
}
private static class LegacyInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private LegacyInboundConnection(MinecraftConnection connection) {
this.connection = connection;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.empty();
}
@Override
public boolean isActive() {
return !connection.isClosed();
}
@Override
public int getProtocolVersion() {
return 0;
}
}
}

View File

@@ -0,0 +1,38 @@
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 Handshake handshake;
InitialInboundConnection(MinecraftConnection connection, Handshake handshake) {
this.connection = connection;
this.handshake = handshake;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.of(InetSocketAddress.createUnresolved(handshake.getServerAddress(), handshake.getPort()));
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
}
@Override
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
}

View File

@@ -1,8 +1,15 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
import com.velocitypowered.api.event.player.GameProfileRequestEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
@@ -10,8 +17,9 @@ import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.api.server.ServerInfo;
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.TextComponent;
import net.kyori.text.format.TextColor;
@@ -19,6 +27,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
@@ -30,82 +39,115 @@ 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 final MinecraftConnection inbound;
private final InboundConnection apiInbound;
private ServerLogin login;
private byte[] verify;
private int playerInfoId;
public LoginSessionHandler(MinecraftConnection inbound) {
public LoginSessionHandler(MinecraftConnection inbound, InboundConnection apiInbound) {
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
this.apiInbound = Preconditions.checkNotNull(apiInbound, "apiInbound");
}
@Override
public void activated() {
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
LoginPluginMessage message = new LoginPluginMessage();
playerInfoId = ThreadLocalRandom.current().nextInt();
message.setId(playerInfoId);
message.setChannel(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL);
message.setData(Unpooled.EMPTY_BUFFER);
inbound.write(message);
}
}
@Override
public void handle(MinecraftPacket packet) throws Exception {
public void handle(MinecraftPacket packet) {
if (packet instanceof LoginPluginResponse) {
LoginPluginResponse lpr = (LoginPluginResponse) packet;
if (lpr.getId() == playerInfoId && lpr.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)
));
if (lpr.getId() == playerInfoId) {
if (lpr.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();
}
}
} else if (packet instanceof ServerLogin) {
this.login = (ServerLogin) packet;
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
LoginPluginMessage message = new LoginPluginMessage();
playerInfoId = ThreadLocalRandom.current().nextInt();
message.setId(playerInfoId);
message.setChannel(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL);
message.setData(Unpooled.EMPTY_BUFFER);
inbound.write(message);
} else {
// Offline-mode, don't try to request encryption.
handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername()));
beginPreLogin();
}
} else if (packet instanceof EncryptionResponse) {
KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair();
EncryptionResponse response = (EncryptionResponse) packet;
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken());
if (!Arrays.equals(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
try {
KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair();
EncryptionResponse response = (EncryptionResponse) packet;
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken());
if (!Arrays.equals(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
}
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret());
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString();
VelocityServer.getServer().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;
}
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
initializePlayer(VelocityServer.GSON.fromJson(profileResponse, GameProfile.class), true);
}, inbound.getChannel().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.close();
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret());
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString();
VelocityServer.getServer().getHttpClient()
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
.thenAcceptAsync(profileResponse -> {
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class);
handleSuccessfulLogin(profile);
}, inbound.getChannel().eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
inbound.close();
return null;
});
}
}
private void beginPreLogin() {
PreLoginEvent event = new PreLoginEvent(apiInbound, login.getUsername());
VelocityServer.getServer().getEventManager().fire(event)
.thenRunAsync(() -> {
if (inbound.isClosed()) {
// The player was disconnected
return;
}
PreLoginComponentResult result = event.getResult();
if (!result.isAllowed()) {
// The component is guaranteed to be provided if the connection was denied.
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
return;
}
if (VelocityServer.getServer().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.getChannel().eventLoop());
}
private EncryptionRequest generateRequest() {
byte[] verify = new byte[4];
ThreadLocalRandom.current().nextBytes(verify);
@@ -116,9 +158,40 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
return request;
}
private void handleSuccessfulLogin(GameProfile profile) {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(profile, inbound);
private void initializePlayer(GameProfile profile, boolean onlineMode) {
GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, onlineMode);
VelocityServer.getServer().getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(profileEvent.getGameProfile(), inbound,
apiInbound.getVirtualHost().orElse(null));
return VelocityServer.getServer().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 VelocityServer.getServer().getEventManager().fire(new LoginEvent(player));
})
// then complete the connection
.thenAcceptAsync(event -> {
if (inbound.isClosed()) {
// The player was disconnected
return;
}
if (!event.getResult().isAllowed()) {
// The component is guaranteed to be provided if the connection was denied.
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
return;
}
handleProxyLogin(player);
}, inbound.getChannel().eventLoop());
});
}
private void handleProxyLogin(ConnectedPlayer player) {
Optional<ServerInfo> toTry = player.getNextServerToTry();
if (!toTry.isPresent()) {
player.close(TextComponent.of("No available servers", TextColor.RED));
@@ -132,14 +205,24 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
ServerLoginSuccess success = new ServerLoginSuccess();
success.setUsername(profile.getName());
success.setUuid(profile.idAsUuid());
success.setUsername(player.getUsername());
success.setUuid(player.getUniqueId());
inbound.write(success);
logger.info("{} has connected", player);
inbound.setAssociation(player);
inbound.setState(StateRegistry.PLAY);
if (!VelocityServer.getServer().registerConnection(player)) {
inbound.closeWith(Disconnect.create(TextComponent.of("You are already on this proxy!", TextColor.RED)));
}
logger.info("{} has connected", player);
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
player.createConnectionRequest(toTry.get()).fireAndForget();
}
@Override
public void handleUnknown(ByteBuf buf) {
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
}
}

View File

@@ -1,28 +1,34 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
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.connection.MinecraftConnection;
import com.velocitypowered.proxy.data.ServerPing;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
public class StatusSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection;
private final InboundConnection inboundWrapper;
public StatusSessionHandler(MinecraftConnection connection) {
public StatusSessionHandler(MinecraftConnection connection, InboundConnection inboundWrapper) {
this.connection = connection;
this.inboundWrapper = inboundWrapper;
}
@Override
public void handle(MinecraftPacket packet) {
Preconditions.checkArgument(packet instanceof StatusPing|| packet instanceof StatusRequest,
Preconditions.checkArgument(packet instanceof StatusPing || packet instanceof StatusRequest,
"Unrecognized packet type " + packet.getClass().getName());
if (packet instanceof StatusPing) {
@@ -34,15 +40,22 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
// Status request
ServerPing ping = new ServerPing(
new ServerPing.Version(connection.getProtocolVersion(), "Velocity 1.9-1.13"),
new ServerPing.Players(0, configuration.getShowMaxPlayers()),
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(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
null
configuration.getFavicon()
);
StatusResponse response = new StatusResponse();
response.setStatus(VelocityServer.GSON.toJson(ping));
connection.write(response);
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
VelocityServer.getServer().getEventManager().fire(event)
.thenRunAsync(() -> {
StatusResponse response = new StatusResponse();
response.setStatus(VelocityServer.GSON.toJson(event.getPing()));
connection.write(response);
}, connection.getChannel().eventLoop());
}
@Override

View File

@@ -3,6 +3,7 @@ package com.velocitypowered.proxy.connection.util;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.serializer.ComponentSerializers;
import java.util.Optional;
@@ -42,4 +43,18 @@ public class ConnectionRequestResults {
}
};
}
public static ConnectionRequestBuilder.Result forDisconnect(TextComponent 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

@@ -0,0 +1,52 @@
package com.velocitypowered.proxy.console;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import net.minecrell.terminalconsole.SimpleTerminalConsole;
import org.jline.reader.*;
import java.util.List;
import java.util.Optional;
public final class VelocityConsole extends SimpleTerminalConsole {
private final VelocityServer server;
public VelocityConsole(VelocityServer server) {
this.server = server;
}
@Override
protected LineReader buildReader(LineReaderBuilder builder) {
return super.buildReader(builder
.appName("Velocity")
.completer((reader, parsedLine, list) -> {
Optional<List<String>> o = server.getCommandManager().offerSuggestions(server.getConsoleCommandSource(), parsedLine.line());
o.ifPresent(offers -> {
for (String offer : offers) {
list.add(new Candidate(offer));
}
});
})
);
}
@Override
protected boolean isRunning() {
return !this.server.isShutdown();
}
@Override
protected void runCommand(String command) {
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandSource(), command)) {
server.getConsoleCommandSource().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
}
}
@Override
protected void shutdown() {
this.server.shutdown();
}
}

View File

@@ -1,84 +0,0 @@
package com.velocitypowered.proxy.data;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.proxy.util.UuidUtils;
import java.util.List;
import java.util.UUID;
public class GameProfile {
private final String id;
private final String name;
private final List<Property> properties;
public GameProfile(String id, String name, List<Property> properties) {
this.id = id;
this.name = name;
this.properties = ImmutableList.copyOf(properties);
}
public String getId() {
return id;
}
public UUID idAsUuid() {
return UuidUtils.fromUndashed(id);
}
public String getName() {
return name;
}
public List<Property> getProperties() {
return ImmutableList.copyOf(properties);
}
public static GameProfile forOfflinePlayer(String username) {
Preconditions.checkNotNull(username, "username");
String id = UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username));
return new GameProfile(id, username, ImmutableList.of());
}
@Override
public String toString() {
return "GameProfile{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", properties=" + properties +
'}';
}
public class Property {
private final String name;
private final String value;
private final String signature;
public Property(String name, String value, String signature) {
this.name = name;
this.value = value;
this.signature = signature;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public String getSignature() {
return signature;
}
@Override
public String toString() {
return "Property{" +
"name='" + name + '\'' +
", value='" + value + '\'' +
", signature='" + signature + '\'' +
'}';
}
}
}

View File

@@ -1,95 +0,0 @@
package com.velocitypowered.proxy.data;
import net.kyori.text.Component;
public class ServerPing {
private final Version version;
private final Players players;
private final Component description;
private final String favicon;
public ServerPing(Version version, Players players, Component description, String favicon) {
this.version = version;
this.players = players;
this.description = description;
this.favicon = favicon;
}
public Version getVersion() {
return version;
}
public Players getPlayers() {
return players;
}
public Component getDescription() {
return description;
}
public String getFavicon() {
return favicon;
}
@Override
public String toString() {
return "ServerPing{" +
"version=" + version +
", players=" + players +
", description=" + description +
", favicon='" + favicon + '\'' +
'}';
}
public static class Version {
private final int protocol;
private final String name;
public Version(int protocol, String name) {
this.protocol = protocol;
this.name = name;
}
public int getProtocol() {
return protocol;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Version{" +
"protocol=" + protocol +
", name='" + name + '\'' +
'}';
}
}
public static class Players {
private final int online;
private final int max;
public Players(int online, int max) {
this.online = online;
this.max = max;
}
public int getOnline() {
return online;
}
public int getMax() {
return max;
}
@Override
public String toString() {
return "Players{" +
"online=" + online +
", max=" + max +
'}';
}
}
}

View File

@@ -0,0 +1,78 @@
package com.velocitypowered.proxy.messages;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.messages.*;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class VelocityChannelRegistrar implements ChannelRegistrar {
private static final Logger logger = LogManager.getLogger(VelocityChannelRegistrar.class);
private final Map<String, MessageHandler> handlers = new ConcurrentHashMap<>();
private final Map<String, ChannelIdentifier> identifierMap = new ConcurrentHashMap<>();
@Override
public void register(MessageHandler handler, ChannelIdentifier... identifiers) {
for (ChannelIdentifier identifier : identifiers) {
Preconditions.checkArgument(identifier instanceof LegacyChannelIdentifier || identifier instanceof MinecraftChannelIdentifier,
"identifier is unknown");
}
for (ChannelIdentifier identifier : identifiers) {
handlers.put(identifier.getId(), handler);
identifierMap.put(identifier.getId(), identifier);
}
}
public MessageHandler.ForwardStatus handlePluginMessage(ChannelMessageSource source, ChannelSide side, PluginMessage message) {
MessageHandler handler = handlers.get(message.getChannel());
ChannelIdentifier identifier = identifierMap.get(message.getChannel());
if (handler == null || identifier == null) {
return MessageHandler.ForwardStatus.FORWARD;
}
try {
return handler.handle(source, side, identifier, message.getData());
} catch (Exception e) {
logger.info("Unable to handle plugin message on channel {} for {}", message.getChannel(), source);
// In case of doubt, do not forward the message on.
return MessageHandler.ForwardStatus.HANDLED;
}
}
@Override
public void unregister(ChannelIdentifier... identifiers) {
for (ChannelIdentifier identifier : identifiers) {
Preconditions.checkArgument(identifier instanceof LegacyChannelIdentifier || identifier instanceof MinecraftChannelIdentifier,
"identifier is unknown");
}
for (ChannelIdentifier identifier : identifiers) {
handlers.remove(identifier.getId());
identifierMap.remove(identifier.getId());
}
}
public Collection<String> getLegacyChannelIds() {
return identifierMap.values().stream()
.filter(i -> i instanceof LegacyChannelIdentifier)
.map(ChannelIdentifier::getId)
.collect(Collectors.toList());
}
public Collection<String> getModernChannelIds() {
return identifierMap.values().stream()
.filter(i -> i instanceof MinecraftChannelIdentifier)
.map(ChannelIdentifier::getId)
.collect(Collectors.toList());
}
public boolean registered(String id) {
return identifierMap.containsKey(id);
}
}

View File

@@ -1,18 +1,18 @@
package com.velocitypowered.network;
package com.velocitypowered.proxy.network;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler;
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
import com.velocitypowered.proxy.protocol.netty.LegacyPingEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
@@ -21,71 +21,53 @@ import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.kqueue.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.ServerSocketChannel;
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 io.netty.handler.timeout.ReadTimeoutHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.network.Connections.CLIENT_READ_TIMEOUT_SECONDS;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.network.Connections.LEGACY_PING_DECODER;
import static com.velocitypowered.network.Connections.LEGACY_PING_ENCODER;
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.network.Connections.READ_TIMEOUT;
import static com.velocitypowered.proxy.network.Connections.*;
public final class ConnectionManager {
private static final Logger logger = LogManager.getLogger(ConnectionManager.class);
private static final String DISABLE_EPOLL_PROPERTY = "velocity.connection.disable-epoll";
private static final boolean DISABLE_EPOLL = Boolean.getBoolean(DISABLE_EPOLL_PROPERTY);
private final Set<Channel> endpoints = new HashSet<>();
private final Class<? extends ServerSocketChannel> serverSocketChannelClass;
private final Class<? extends SocketChannel> socketChannelClass;
private final TransportType transportType;
private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup;
public ConnectionManager() {
final boolean epoll = canUseEpoll();
if (epoll) {
this.serverSocketChannelClass = EpollServerSocketChannel.class;
this.socketChannelClass = EpollSocketChannel.class;
this.bossGroup = new EpollEventLoopGroup(0, createThreadFactory("Netty Epoll Boss #%d"));
this.workerGroup = new EpollEventLoopGroup(0, createThreadFactory("Netty Epoll Worker #%d"));
} else {
this.serverSocketChannelClass = NioServerSocketChannel.class;
this.socketChannelClass = NioSocketChannel.class;
this.bossGroup = new NioEventLoopGroup(0, createThreadFactory("Netty Nio Boss #%d"));
this.workerGroup = new NioEventLoopGroup(0, createThreadFactory("Netty Nio Worker #%d"));
}
this.logChannelInformation(epoll);
this.transportType = TransportType.bestType();
this.bossGroup = transportType.createEventLoopGroup(true);
this.workerGroup = transportType.createEventLoopGroup(false);
this.logChannelInformation();
}
private void logChannelInformation(final boolean epoll) {
final StringBuilder sb = new StringBuilder();
sb.append("Using channel type ");
sb.append(epoll ? "epoll": "nio");
if(DISABLE_EPOLL) {
sb.append(String.format(" - epoll explicitly disabled using -D%s=true", DISABLE_EPOLL_PROPERTY));
}
logger.info(sb.toString()); // TODO: move to logger
private void logChannelInformation() {
logger.info("Connections will use {} channels, {} compression, {} ciphers", transportType, Natives.compressor.getLoadedVariant(), Natives.cipher.getLoadedVariant());
}
public void bind(final InetSocketAddress address) {
final ServerBootstrap bootstrap = new ServerBootstrap()
.channel(this.serverSocketChannelClass)
.channel(this.transportType.serverSocketChannelClass)
.group(this.bossGroup, this.workerGroup)
.childHandler(new ChannelInitializer<Channel>() {
@Override
@@ -120,9 +102,27 @@ public final class ConnectionManager {
});
}
public void queryBind(final String hostname, final int port) {
Bootstrap bootstrap = new Bootstrap()
.channel(transportType.datagramChannelClass)
.group(this.workerGroup)
.handler(new GS4QueryHandler())
.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 Bootstrap createWorker() {
return new Bootstrap()
.channel(this.socketChannelClass)
.channel(this.transportType.socketChannelClass)
.group(this.workerGroup);
}
@@ -137,14 +137,61 @@ public final class ConnectionManager {
}
}
private static boolean canUseEpoll() {
return Epoll.isAvailable() && !DISABLE_EPOLL;
}
private static ThreadFactory createThreadFactory(final String nameFormat) {
return new ThreadFactoryBuilder()
.setNameFormat(nameFormat)
.setDaemon(true)
.build();
}
private enum TransportType {
NIO(NioServerSocketChannel.class, NioSocketChannel.class, NioDatagramChannel.class) {
@Override
public EventLoopGroup createEventLoopGroup(boolean boss) {
String name = "Netty NIO " + (boss ? "Boss" : "Worker") + " #%d";
return new NioEventLoopGroup(0, createThreadFactory(name));
}
},
EPOLL(EpollServerSocketChannel.class, EpollSocketChannel.class, EpollDatagramChannel.class) {
@Override
public EventLoopGroup createEventLoopGroup(boolean boss) {
String name = "Netty Epoll " + (boss ? "Boss" : "Worker") + " #%d";
return new EpollEventLoopGroup(0, createThreadFactory(name));
}
},
KQUEUE(KQueueServerSocketChannel.class, KQueueSocketChannel.class, KQueueDatagramChannel.class) {
@Override
public EventLoopGroup createEventLoopGroup(boolean boss) {
String name = "Netty KQueue " + (boss ? "Boss" : "Worker") + " #%d";
return new KQueueEventLoopGroup(0, createThreadFactory(name));
}
};
private final Class<? extends ServerSocketChannel> serverSocketChannelClass;
private final Class<? extends SocketChannel> socketChannelClass;
private final Class<? extends DatagramChannel> datagramChannelClass;
TransportType(Class<? extends ServerSocketChannel> serverSocketChannelClass, Class<? extends SocketChannel> socketChannelClass, Class<? extends DatagramChannel> datagramChannelClass) {
this.serverSocketChannelClass = serverSocketChannelClass;
this.socketChannelClass = socketChannelClass;
this.datagramChannelClass = datagramChannelClass;
}
@Override
public String toString() {
return name().toLowerCase(Locale.US);
}
public abstract EventLoopGroup createEventLoopGroup(boolean boss);
public static TransportType bestType() {
if (Epoll.isAvailable()) {
return EPOLL;
} else if (KQueue.isAvailable()) {
return KQUEUE;
} else {
return NIO;
}
}
}
}

View File

@@ -1,4 +1,4 @@
package com.velocitypowered.network;
package com.velocitypowered.proxy.network;
public interface Connections {
String CIPHER_DECODER = "cipher-decoder";

View File

@@ -0,0 +1,56 @@
package com.velocitypowered.proxy.plugin;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
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();
}
public PluginClassLoader(URL[] urls) {
super(urls);
loaders.add(this);
}
public 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
}
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);
}
}

View File

@@ -0,0 +1,192 @@
package com.velocitypowered.proxy.plugin;
import com.google.common.base.Preconditions;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.velocitypowered.api.event.EventHandler;
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 com.velocitypowered.proxy.util.concurrency.RecordingThreadFactory;
import net.kyori.event.EventSubscriber;
import net.kyori.event.PostResult;
import net.kyori.event.SimpleEventBus;
import net.kyori.event.method.*;
import net.kyori.event.method.asm.ASMEventExecutorFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.concurrent.*;
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 VelocityEventBus bus = new VelocityEventBus(
new ASMEventExecutorFactory<>(new PluginClassLoader(new URL[0])),
new VelocityMethodScanner());
private final ExecutorService service;
private final RecordingThreadFactory recordingThreadFactory;
private final PluginManager pluginManager;
public VelocityEventManager(PluginManager pluginManager) {
this.pluginManager = pluginManager;
this.recordingThreadFactory = new RecordingThreadFactory(new ThreadFactoryBuilder()
.setNameFormat("Velocity Event Executor - #%d").setDaemon(true).build());
this.service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), recordingThreadFactory);
}
@Override
public void register(@NonNull Object plugin, @NonNull Object listener) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(listener, "listener");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
registeredListenersByPlugin.put(plugin, listener);
bus.register(listener);
}
@Override
public <E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler<E> handler) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(eventClass, "eventClass");
Preconditions.checkNotNull(postOrder, "postOrder");
Preconditions.checkNotNull(handler, "listener");
bus.register(eventClass, new KyoriToVelocityHandler<>(handler, postOrder));
}
@Override
public <E> @NonNull CompletableFuture<E> fire(@NonNull E event) {
Preconditions.checkNotNull(event, "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);
}
}
};
if (recordingThreadFactory.currentlyInFactory()) {
// Optimization: fire the event immediately, we are on the event handling thread.
runEvent.run();
return CompletableFuture.completedFuture(event);
}
CompletableFuture<E> eventFuture = new CompletableFuture<>();
service.execute(() -> {
runEvent.run();
eventFuture.complete(event);
});
return eventFuture;
}
@Override
public void unregisterListeners(@NonNull Object plugin) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
Collection<Object> listeners = registeredListenersByPlugin.removeAll(plugin);
listeners.forEach(bus::unregister);
Collection<EventHandler<?>> handlers = registeredHandlersByPlugin.removeAll(plugin);
handlers.forEach(bus::unregister);
}
@Override
public void unregisterListener(@NonNull Object plugin, @NonNull Object listener) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(listener, "listener");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
registeredListenersByPlugin.remove(plugin, listener);
bus.unregister(listener);
}
@Override
public <E> void unregister(@NonNull Object plugin, @NonNull EventHandler<E> handler) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(handler, "listener");
registeredHandlersByPlugin.remove(plugin, handler);
bus.unregister(handler);
}
public boolean shutdown() throws InterruptedException {
service.shutdown();
return service.awaitTermination(10, TimeUnit.SECONDS);
}
private static class VelocityEventBus extends SimpleEventBus<Object> {
private final MethodSubscriptionAdapter<Object> methodAdapter;
VelocityEventBus(EventExecutor.@NonNull Factory<Object, Object> factory, @NonNull MethodScanner<Object> methodScanner) {
super(Object.class);
this.methodAdapter = new SimpleMethodSubscriptionAdapter<>(this, factory, methodScanner);
}
void register(Object listener) {
this.methodAdapter.register(listener);
}
void unregister(Object listener) {
this.methodAdapter.unregister(listener);
}
void unregister(EventHandler<?> handler) {
this.unregister(s -> s instanceof KyoriToVelocityHandler && ((KyoriToVelocityHandler<?>) s).getHandler().equals(handler));
}
}
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) {
return method.getAnnotation(Subscribe.class).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;
}
}
}

View File

@@ -0,0 +1,128 @@
package com.velocitypowered.proxy.plugin;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginManager;
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 org.checkerframework.checker.nullness.qual.NonNull;
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;
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;
public VelocityPluginManager(VelocityServer server) {
this.server = checkNotNull(server, "server");
}
private void registerPlugin(@NonNull PluginContainer plugin) {
plugins.put(plugin.getDescription().getId(), plugin);
plugin.getInstance().ifPresent(instance -> pluginInstances.put(instance, plugin));
}
public void loadPlugins(@NonNull Path directory) throws IOException {
checkNotNull(directory, "directory");
checkArgument(Files.isDirectory(directory), "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 -> Files.isRegularFile(p) && 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);
}
}
}
if (found.isEmpty()) {
// No plugins found
return;
}
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found);
// 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;
}
}
// Actually create the plugin
PluginContainer pluginObject;
try {
pluginObject = loader.createPlugin(plugin);
} catch (Exception e) {
logger.error("Can't create plugin {}", plugin.getId(), e);
continue;
}
registerPlugin(pluginObject);
}
}
@Override
public @NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance) {
checkNotNull(instance, "instance");
if (instance instanceof PluginContainer) {
return Optional.of((PluginContainer) instance);
}
return Optional.ofNullable(pluginInstances.get(instance));
}
@Override
public @NonNull Optional<PluginContainer> getPlugin(@NonNull String id) {
checkNotNull(id, "id");
return Optional.ofNullable(plugins.get(id));
}
@Override
public @NonNull Collection<PluginContainer> getPlugins() {
return Collections.unmodifiableCollection(plugins.values());
}
@Override
public boolean isLoaded(@NonNull String id) {
return plugins.containsKey(id);
}
@Override
public void addToClasspath(@NonNull Object plugin, @NonNull 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

@@ -0,0 +1,124 @@
package com.velocitypowered.proxy.plugin.loader;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.velocitypowered.api.plugin.*;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import com.velocitypowered.api.proxy.ProxyServer;
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.SerializedPluginDescription;
import com.velocitypowered.proxy.plugin.loader.java.VelocityPluginModule;
import java.io.BufferedInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Pattern;
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;
}
@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.");
}
PluginClassLoader loader = new PluginClassLoader(
new URL[] {source.toUri().toURL() }
);
Class mainClass = loader.loadClass(serialized.get().getMain());
VelocityPluginDescription description = createDescription(serialized.get(), source, mainClass);
String pluginId = description.getId();
Pattern pattern = PluginDescription.ID_PATTERN;
if (!pattern.matcher(pluginId).matches()) {
throw new InvalidPluginException("Plugin ID '" + pluginId + "' must match pattern " + pattern.pattern());
}
return description;
}
@Override
public PluginContainer createPlugin(PluginDescription description) throws Exception {
if (!(description instanceof JavaVelocityPluginDescription)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
}
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
Optional<Path> source = javaDescription.getSource();
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());
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<>();
if (description.getDependencies() != null) {
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()
);
}
}

View File

@@ -0,0 +1,15 @@
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;
}

View File

@@ -0,0 +1,26 @@
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;
}
@Override
public PluginDescription getDescription() {
return this.description;
}
@Override
public Optional<?> getInstance() {
return Optional.ofNullable(instance);
}
}

View File

@@ -0,0 +1,98 @@
package com.velocitypowered.proxy.plugin.loader;
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;
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, PluginDependency::getId);
this.source = source;
}
@Override
public String getId() {
return id;
}
@Override
public Optional<String> getName() {
return Optional.ofNullable(name);
}
@Override
public Optional<String> getVersion() {
return Optional.ofNullable(version);
}
@Override
public Optional<String> getDescription() {
return Optional.ofNullable(description);
}
@Override
public Optional<String> getUrl() {
return Optional.ofNullable(url);
}
@Override
public List<String> getAuthors() {
return authors;
}
@Override
public Collection<PluginDependency> getDependencies() {
return dependencies.values();
}
@Override
public Optional<PluginDependency> getDependency(String id) {
return Optional.ofNullable(dependencies.get(id));
}
@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

@@ -0,0 +1,25 @@
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;
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);
}
public Class<?> getMainClass() {
return mainClass;
}
}

View File

@@ -0,0 +1,151 @@
package com.velocitypowered.proxy.plugin.loader.java;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.velocitypowered.api.plugin.Plugin;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class SerializedPluginDescription {
// @Nullable is used here to make GSON skip these in the serialized file
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 @Nullable List<String> authors;
private final @Nullable List<Dependency> dependencies;
private final String main;
public SerializedPluginDescription(String id, String name, String version, String description, String url,
List<String> authors, List<Dependency> dependencies, String main) {
this.id = Preconditions.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 || authors.isEmpty() ? null : authors;
this.dependencies = dependencies == null || dependencies.isEmpty() ? null : dependencies;
this.main = Preconditions.checkNotNull(main, "main");
}
public static SerializedPluginDescription from(Plugin plugin, String qualifiedName) {
List<Dependency> dependencies = new ArrayList<>();
for (com.velocitypowered.api.plugin.Dependency dependency : plugin.dependencies()) {
dependencies.add(new Dependency(dependency.id(), dependency.optional()));
}
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), plugin.description(), plugin.url(),
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty()).collect(Collectors.toList()), dependencies, qualifiedName);
}
public String getId() {
return id;
}
public @Nullable String getName() {
return name;
}
public @Nullable String getVersion() {
return version;
}
public @Nullable String getDescription() {
return description;
}
public @Nullable String getUrl() {
return url;
}
public @Nullable List<String> getAuthors() {
return authors;
}
public @Nullable List<Dependency> getDependencies() {
return dependencies;
}
public String getMain() {
return main;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SerializedPluginDescription that = (SerializedPluginDescription) o;
return Objects.equals(id, that.id) &&
Objects.equals(name, that.name) &&
Objects.equals(version, that.version) &&
Objects.equals(description, that.description) &&
Objects.equals(url, that.url) &&
Objects.equals(authors, that.authors) &&
Objects.equals(dependencies, that.dependencies) &&
Objects.equals(main, that.main);
}
@Override
public int hashCode() {
return Objects.hash(id, name, version, description, url, authors, dependencies);
}
@Override
public String toString() {
return "SerializedPluginDescription{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", version='" + version + '\'' +
", description='" + description + '\'' +
", url='" + url + '\'' +
", authors=" + authors +
", dependencies=" + dependencies +
", main='" + main + '\'' +
'}';
}
public static class Dependency {
private final String id;
private final boolean optional;
public Dependency(String id, boolean optional) {
this.id = id;
this.optional = optional;
}
public String getId() {
return id;
}
public boolean isOptional() {
return optional;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dependency that = (Dependency) o;
return optional == that.optional &&
Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id, optional);
}
@Override
public String toString() {
return "Dependency{" +
"id='" + id + '\'' +
", optional=" + optional +
'}';
}
}
}

View File

@@ -0,0 +1,38 @@
package com.velocitypowered.proxy.plugin.loader.java;
import com.google.inject.Binder;
import com.google.inject.Module;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.event.EventManager;
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 com.velocitypowered.proxy.VelocityServer;
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;
}
@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

@@ -0,0 +1,69 @@
package com.velocitypowered.proxy.plugin.util;
import com.google.common.collect.Maps;
import com.google.common.graph.Graph;
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.*;
public class PluginDependencyUtils {
public static List<PluginDescription> sortCandidates(List<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, PluginDescription::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);
}
}
}
// 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.poll();
sorted.add(candidate);
for (PluginDescription node : graph.successors(candidate)) {
graph.removeEdge(node, candidate);
if (graph.adjacentNodes(node).isEmpty()) {
if (!noEdges.contains(node)) {
noEdges.add(node);
}
}
}
}
if (!graph.edges().isEmpty()) {
throw new IllegalStateException("Plugin circular dependency found: " + graph.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);
}
}
return found;
}
}

View File

@@ -1,30 +0,0 @@
package com.velocitypowered.proxy.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
public class PacketWrapper {
private final MinecraftPacket packet;
private final ByteBuf buffer;
public PacketWrapper(MinecraftPacket packet, ByteBuf buffer) {
this.packet = packet;
this.buffer = buffer;
}
public MinecraftPacket getPacket() {
return packet;
}
public ByteBuf getBuffer() {
return buffer;
}
@Override
public String toString() {
return "PacketWrapper{" +
"packet=" + packet +
", buffer=" + ByteBufUtil.hexDump(buffer) +
'}';
}
}

View File

@@ -1,8 +1,11 @@
package com.velocitypowered.proxy.protocol;
import java.util.Arrays;
import com.google.common.primitives.ImmutableIntArray;
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;
@@ -14,10 +17,15 @@ public enum ProtocolConstants { ;
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 MINIMUM_GENERIC_VERSION = MINECRAFT_1_9;
public static final int MINIMUM_GENERIC_VERSION = MINECRAFT_1_8;
public static final int MAXIMUM_GENERIC_VERSION = MINECRAFT_1_13_1;
public static final int[] SUPPORTED_VERSIONS = new int[] {
public static final String SUPPORTED_GENERIC_VERSION_STRING = "1.8-1.13.1";
public static final ImmutableIntArray SUPPORTED_VERSIONS = ImmutableIntArray.of(
MINECRAFT_1_8,
MINECRAFT_1_9,
MINECRAFT_1_9_1,
MINECRAFT_1_9_2,
@@ -28,11 +36,12 @@ public enum ProtocolConstants { ;
MINECRAFT_1_12,
MINECRAFT_1_12_1,
MINECRAFT_1_12_2,
MINECRAFT_1_13
};
MINECRAFT_1_13,
MINECRAFT_1_13_1
);
public static boolean isSupported(int version) {
return Arrays.binarySearch(SUPPORTED_VERSIONS, version) >= 0;
return SUPPORTED_VERSIONS.contains(version);
}
public enum Direction {

View File

@@ -1,8 +1,11 @@
package com.velocitypowered.proxy.protocol;
import com.google.common.primitives.ImmutableIntArray;
import com.velocitypowered.proxy.protocol.packet.*;
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 java.util.*;
import java.util.function.Supplier;
@@ -10,6 +13,7 @@ import java.util.function.Supplier;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.*;
public enum StateRegistry {
HANDSHAKE {
{
SERVERBOUND.register(Handshake.class, Handshake::new,
@@ -31,75 +35,108 @@ public enum StateRegistry {
},
PLAY {
{
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),
map(0x05, MINECRAFT_1_13, false));
SERVERBOUND.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_9),
map(0x03, MINECRAFT_1_12),
map(0x02, MINECRAFT_1_12_2),
map(0x02, MINECRAFT_1_13));
map(0x01, MINECRAFT_1_8, false),
map(0x02, MINECRAFT_1_9, false),
map(0x03, MINECRAFT_1_12, false),
map(0x02, MINECRAFT_1_12_2, false),
map(0x02, MINECRAFT_1_13, false));
SERVERBOUND.register(ClientSettings.class, ClientSettings::new,
map(0x04, MINECRAFT_1_9),
map(0x05, MINECRAFT_1_12),
map(0x04, MINECRAFT_1_12_1),
map(0x04, MINECRAFT_1_13));
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(0x09, MINECRAFT_1_9),
map(0x0A, MINECRAFT_1_12),
map(0x09, MINECRAFT_1_12_1),
map(0x0A, MINECRAFT_1_13));
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(0x0B, MINECRAFT_1_9),
map(0x0C, MINECRAFT_1_12),
map(0x0B, MINECRAFT_1_12_1),
map(0x0E, MINECRAFT_1_13));
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(BossBar.class, BossBar::new,
map(0x0C, MINECRAFT_1_9),
map(0x0C, MINECRAFT_1_12));
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(0x0F, MINECRAFT_1_9),
map(0x0F, MINECRAFT_1_12),
map(0x0E, MINECRAFT_1_13));
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, true),
map(0x0E, MINECRAFT_1_9, true),
map(0x0E, MINECRAFT_1_12, true),
map(0x10, MINECRAFT_1_13, true));
CLIENTBOUND.register(PluginMessage.class, PluginMessage::new,
map(0x18, MINECRAFT_1_9),
map(0x18, MINECRAFT_1_12),
map(0x19, MINECRAFT_1_13));
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(0x1A, MINECRAFT_1_9),
map(0x1A, MINECRAFT_1_12),
map(0x1B, MINECRAFT_1_13));
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(0x1F, MINECRAFT_1_9),
map(0x1F, MINECRAFT_1_12),
map(0x21, MINECRAFT_1_13));
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(0x23, MINECRAFT_1_9),
map(0x23, MINECRAFT_1_12),
map(0x25, MINECRAFT_1_13));
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(0x33, MINECRAFT_1_9),
map(0x34, MINECRAFT_1_12),
map(0x35, MINECRAFT_1_12_2),
map(0x38, MINECRAFT_1_13));
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(ScoreboardDisplay.class, ScoreboardDisplay::new,
map(0x38, MINECRAFT_1_9),
map(0x3A, MINECRAFT_1_12),
map(0x3B, MINECRAFT_1_12_1),
map(0x3E, MINECRAFT_1_13));
map(0x3D, MINECRAFT_1_8, true),
map(0x38, MINECRAFT_1_9, true),
map(0x3A, MINECRAFT_1_12, true),
map(0x3B, MINECRAFT_1_12_1, true),
map(0x3E, MINECRAFT_1_13, true));
CLIENTBOUND.register(ScoreboardObjective.class, ScoreboardObjective::new,
map(0x3F, MINECRAFT_1_9),
map(0x41, MINECRAFT_1_12),
map(0x42, MINECRAFT_1_12_1),
map(0x45, MINECRAFT_1_13));
map(0x3B, MINECRAFT_1_8, true),
map(0x3F, MINECRAFT_1_9, true),
map(0x41, MINECRAFT_1_12, true),
map(0x42, MINECRAFT_1_12_1, true),
map(0x45, MINECRAFT_1_13, true));
CLIENTBOUND.register(ScoreboardTeam.class, ScoreboardTeam::new,
map(0x41, MINECRAFT_1_9),
map(0x43, MINECRAFT_1_12),
map(0x44, MINECRAFT_1_12_1),
map(0x47, MINECRAFT_1_13));
map(0x3E, MINECRAFT_1_8, true),
map(0x41, MINECRAFT_1_9, true),
map(0x43, MINECRAFT_1_12, true),
map(0x44, MINECRAFT_1_12_1, true),
map(0x47, MINECRAFT_1_13, true));
CLIENTBOUND.register(ScoreboardSetScore.class, ScoreboardSetScore::new,
map(0x42, MINECRAFT_1_9),
map(0x44, MINECRAFT_1_12),
map(0x45, MINECRAFT_1_12_1),
map(0x48, MINECRAFT_1_13));
map(0x3C, MINECRAFT_1_8, true),
map(0x42, MINECRAFT_1_9, true),
map(0x44, MINECRAFT_1_12, true),
map(0x45, MINECRAFT_1_12_1, true),
map(0x48, MINECRAFT_1_13, true));
}
},
LOGIN {
@@ -109,7 +146,7 @@ public enum StateRegistry {
SERVERBOUND.register(EncryptionResponse.class, EncryptionResponse::new,
genericMappings(0x01));
SERVERBOUND.register(LoginPluginResponse.class, LoginPluginResponse::new,
map(0x02, MINECRAFT_1_13));
map(0x02, MINECRAFT_1_13, false));
CLIENTBOUND.register(Disconnect.class, Disconnect::new,
genericMappings(0x00));
@@ -120,7 +157,7 @@ public enum StateRegistry {
CLIENTBOUND.register(SetCompression.class, SetCompression::new,
genericMappings(0x03));
CLIENTBOUND.register(LoginPluginMessage.class, LoginPluginMessage::new,
map(0x04, MINECRAFT_1_13));
map(0x04, MINECRAFT_1_13, false));
}
};
@@ -130,26 +167,24 @@ public enum StateRegistry {
public final PacketRegistry SERVERBOUND = new PacketRegistry(ProtocolConstants.Direction.SERVERBOUND, this);
public static class PacketRegistry {
private static final IntObjectMap<int[]> LINKED_PROTOCOL_VERSIONS = new IntObjectHashMap<>();
private static final IntObjectMap<ImmutableIntArray> LINKED_PROTOCOL_VERSIONS = new IntObjectHashMap<>();
static {
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_9, new int[] { MINECRAFT_1_9_1, MINECRAFT_1_9_2, MINECRAFT_1_9_4 });
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_9_4, new int[] { MINECRAFT_1_10, MINECRAFT_1_11, MINECRAFT_1_11_1 });
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_12, new int[] { MINECRAFT_1_12_1 });
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_12_1, new int[] { MINECRAFT_1_12_2 });
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));
}
private final ProtocolConstants.Direction direction;
private final StateRegistry state;
private final IntObjectMap<ProtocolVersion> versions = new IntObjectHashMap<>();
private final IntObjectMap<ProtocolVersion> versions = new IntObjectHashMap<>(16);
public PacketRegistry(Direction direction, StateRegistry state) {
this.direction = direction;
this.state = state;
for (int version : ProtocolConstants.SUPPORTED_VERSIONS) {
versions.put(version, new ProtocolVersion(version));
}
versions.put(MINIMUM_GENERIC_VERSION, new ProtocolVersion(MINIMUM_GENERIC_VERSION));
ProtocolConstants.SUPPORTED_VERSIONS.forEach(version -> versions.put(version, new ProtocolVersion(version)));
}
public ProtocolVersion getVersion(final int version) {
@@ -163,6 +198,7 @@ public enum StateRegistry {
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.");
@@ -173,18 +209,20 @@ public enum StateRegistry {
if (version == null) {
throw new IllegalArgumentException("Unknown protocol version " + mapping.protocolVersion);
}
version.packetIdToSupplier.put(mapping.id, packetSupplier);
if (!mapping.encodeOnly) {
version.packetIdToSupplier.put(mapping.id, packetSupplier);
}
version.packetClassToId.put(clazz, mapping.id);
int[] linked = LINKED_PROTOCOL_VERSIONS.get(mapping.protocolVersion);
ImmutableIntArray linked = LINKED_PROTOCOL_VERSIONS.get(mapping.protocolVersion);
if (linked != null) {
links: for (int i : linked) {
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 (i == m.protocolVersion) continue links;
if (linkedVersion == m.protocolVersion) continue links;
}
register(clazz, packetSupplier, map(mapping.id, i));
register(clazz, packetSupplier, map(mapping.id, linkedVersion, mapping.encodeOnly));
}
}
}
@@ -192,11 +230,12 @@ public enum StateRegistry {
public class ProtocolVersion {
public final int id;
final IntObjectMap<Supplier<? extends MinecraftPacket>> packetIdToSupplier = new IntObjectHashMap<>();
final Map<Class<? extends MinecraftPacket>, Integer> packetClassToId = new HashMap<>();
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 id) {
this.id = id;
this.packetClassToId.defaultReturnValue(Integer.MIN_VALUE);
}
public MinecraftPacket createPacket(final int id) {
@@ -208,8 +247,8 @@ public enum StateRegistry {
}
public int getPacketId(final MinecraftPacket packet) {
final Integer id = this.packetClassToId.get(packet.getClass());
if (id == null) {
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.id
@@ -231,10 +270,12 @@ public enum StateRegistry {
public static class PacketMapping {
private final int id;
private final int protocolVersion;
public PacketMapping(int id, int protocolVersion) {
private final boolean encodeOnly;
public PacketMapping(int id, int protocolVersion, boolean packetDecoding) {
this.id = id;
this.protocolVersion = protocolVersion;
this.encodeOnly = packetDecoding;
}
@Override
@@ -242,6 +283,7 @@ public enum StateRegistry {
return "PacketMapping{" +
"id=" + id +
", protocolVersion=" + protocolVersion +
", encodeOnly=" + encodeOnly +
'}';
}
@@ -251,24 +293,33 @@ public enum StateRegistry {
if (o == null || getClass() != o.getClass()) return false;
PacketMapping that = (PacketMapping) o;
return id == that.id &&
protocolVersion == that.protocolVersion;
protocolVersion == that.protocolVersion &&
encodeOnly == that.encodeOnly;
}
@Override
public int hashCode() {
return Objects.hash(id, protocolVersion);
return Objects.hash(id, protocolVersion, encodeOnly);
}
}
private static PacketMapping map(int id, int version) {
return new PacketMapping(id, version);
/**
* 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_9),
map(id, MINECRAFT_1_12),
map(id, MINECRAFT_1_13)
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

@@ -0,0 +1,190 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableSet;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
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 java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class GS4QueryHandler extends SimpleChannelInboundHandler<DatagramPacket> {
private static final Logger logger = LogManager.getLogger(GS4QueryHandler.class);
private final static short QUERY_MAGIC_FIRST = 0xFE;
private final static short QUERY_MAGIC_SECOND = 0xFD;
private final static byte QUERY_TYPE_HANDSHAKE = 0x09;
private final static byte QUERY_TYPE_STAT = 0x00;
private final static byte[] QUERY_RESPONSE_FULL_PADDING = new byte[] { 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, (byte) 0x80, 0x00 };
private final static byte[] QUERY_RESPONSE_FULL_PADDING2 = new byte[] { 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00 };
// Contents to add into basic stat response. See ResponseWriter class below
private final static Set<String> QUERY_BASIC_RESPONSE_CONTENTS = ImmutableSet.of(
"hostname",
"gametype",
"map",
"numplayers",
"maxplayers",
"hostport",
"hostip"
);
private final static Cache<InetAddress, Integer> sessions = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
@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));
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");
}
// Packet header
queryResponse.writeByte(QUERY_TYPE_STAT);
queryResponse.writeInt(sessionId);
// Fetch information
VelocityServer server = VelocityServer.getServer();
Collection<Player> players = server.getAllPlayers();
// Start writing the response
ResponseWriter responseWriter = new ResponseWriter(queryResponse, queryMessage.readableBytes() == 0);
responseWriter.write("hostname", ComponentSerializers.PLAIN.serialize(server.getConfiguration().getMotdComponent()));
responseWriter.write("gametype", "SMP");
responseWriter.write("game_id", "MINECRAFT");
responseWriter.write("version", ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING);
responseWriter.write("plugins", "");
responseWriter.write("map", "Velocity");
responseWriter.write("numplayers", players.size());
responseWriter.write("maxplayers", server.getConfiguration().getShowMaxPlayers());
responseWriter.write("hostport", server.getConfiguration().getBind().getPort());
responseWriter.write("hostip", server.getConfiguration().getBind().getHostString());
responseWriter.writePlayers(players);
break;
}
default: {
throw new IllegalStateException("Invalid query type: " + type);
}
}
// Send the response
ctx.writeAndFlush(responsePacket);
} catch (Exception e) {
logger.warn("Error while trying to handle a query packet from {}", msg.sender(), e);
responsePacket.release();
}
}
private static void writeString(ByteBuf buf, String string) {
buf.writeCharSequence(string, StandardCharsets.ISO_8859_1);
buf.writeByte(0x00);
}
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<Player> 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.getUsername()));
buf.writeByte(0x00);
}
}
}

View File

@@ -1,5 +1,6 @@
package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.proxy.protocol.packet.LegacyHandshake;
import com.velocitypowered.proxy.protocol.packet.LegacyPing;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
@@ -19,8 +20,11 @@ public class LegacyPingDecoder extends ByteToMessageDecoder {
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);
}
ctx.pipeline().remove(this);
}
}

View File

@@ -1,39 +1,27 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.proxy.protocol.packet.LegacyPingResponse;
import com.velocitypowered.proxy.protocol.packet.LegacyDisconnect;
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;
import java.util.List;
@ChannelHandler.Sharable
public class LegacyPingEncoder extends MessageToByteEncoder<LegacyPingResponse> {
public class LegacyPingEncoder extends MessageToByteEncoder<LegacyDisconnect> {
public static final LegacyPingEncoder INSTANCE = new LegacyPingEncoder();
private LegacyPingEncoder() {}
@Override
protected void encode(ChannelHandlerContext ctx, LegacyPingResponse msg, ByteBuf out) throws Exception {
protected void encode(ChannelHandlerContext ctx, LegacyDisconnect msg, ByteBuf out) throws Exception {
out.writeByte(0xff);
String serializedResponse = serialize(msg);
out.writeShort(serializedResponse.length());
out.writeBytes(serializedResponse.getBytes(StandardCharsets.UTF_16BE));
writeLegacyString(out, msg.getReason());
}
private String serialize(LegacyPingResponse response) {
List<String> parts = ImmutableList.of(
"§1",
Integer.toString(response.getProtocolVersion()),
response.getServerVersion(),
response.getMotd(),
Integer.toString(response.getPlayersOnline()),
Integer.toString(response.getPlayersMax())
);
return Joiner.on('\0').join(parts);
private static void writeLegacyString(ByteBuf out, String string) {
out.writeShort(string.length());
out.writeBytes(string.getBytes(StandardCharsets.UTF_16BE));
}
}

View File

@@ -28,6 +28,15 @@ public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
}
}
@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

@@ -26,13 +26,13 @@ public class MinecraftDecoder extends MessageToMessageDecoder<ByteBuf> {
return;
}
ByteBuf slice = msg.retainedSlice();
ByteBuf slice = msg.slice();
int packetId = ProtocolUtils.readVarInt(msg);
MinecraftPacket packet = this.protocolVersion.createPacket(packetId);
if (packet == null) {
msg.skipBytes(msg.readableBytes());
out.add(new PacketWrapper(null, slice));
out.add(slice.retain());
} else {
try {
packet.decode(msg, direction, protocolVersion.id);
@@ -40,7 +40,11 @@ public class MinecraftDecoder extends MessageToMessageDecoder<ByteBuf> {
throw new CorruptedFrameException("Error decoding " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion + " State " + state + " ID " + Integer.toHexString(packetId), e);
}
out.add(new PacketWrapper(packet, slice));
if (msg.isReadable()) {
throw new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion + " State " + state + " ID " + Integer.toHexString(packetId));
}
out.add(packet);
}
}

View File

@@ -80,7 +80,10 @@ public class ClientSettings implements MinecraftPacket {
this.chatVisibility = ProtocolUtils.readVarInt(buf);
this.chatColors = buf.readBoolean();
this.skinParts = buf.readUnsignedByte();
this.mainHand = ProtocolUtils.readVarInt(buf);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) {
this.mainHand = ProtocolUtils.readVarInt(buf);
}
}
@Override
@@ -90,6 +93,9 @@ public class ClientSettings implements MinecraftPacket {
ProtocolUtils.writeVarInt(buf, chatVisibility);
buf.writeBoolean(chatColors);
buf.writeByte(skinParts);
ProtocolUtils.writeVarInt(buf, mainHand);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) {
ProtocolUtils.writeVarInt(buf, mainHand);
}
}
}

View File

@@ -0,0 +1,62 @@
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants.Direction;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.writeString;
import io.netty.buffer.ByteBuf;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializer;
import net.kyori.text.serializer.ComponentSerializers;
public class HeaderAndFooter implements MinecraftPacket {
private static final HeaderAndFooter RESET = new HeaderAndFooter("{\"translate\":\"\"}", "{\"translate\":\"\"}");
private String header;
private String footer;
public HeaderAndFooter() {
}
public HeaderAndFooter(String header, String footer) {
this.header = header;
this.footer = footer;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
public String getFooter() {
return footer;
}
public void setFooter(String footer) {
this.footer = footer;
}
@Override
public void decode(ByteBuf buf, Direction direction, int protocolVersion) {
throw new UnsupportedOperationException("Decode is not implemented");
}
@Override
public void encode(ByteBuf buf, Direction direction, int protocolVersion) {
writeString(buf, header);
writeString(buf, footer);
}
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

@@ -87,7 +87,11 @@ public class JoinGame implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.entityId = buf.readInt();
this.gamemode = buf.readUnsignedByte();
this.dimension = buf.readInt();
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);
@@ -98,7 +102,11 @@ public class JoinGame implements MinecraftPacket {
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeInt(entityId);
buf.writeByte(gamemode);
buf.writeInt(dimension);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9_1) {
buf.writeInt(dimension);
} else {
buf.writeByte(dimension);
}
buf.writeByte(difficulty);
buf.writeByte(maxPlayers);
ProtocolUtils.writeString(buf, levelType);

View File

@@ -0,0 +1,32 @@
package com.velocitypowered.proxy.protocol.packet;
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;
}
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 static LegacyDisconnect from(TextComponent component) {
return new LegacyDisconnect(ComponentSerializers.LEGACY.serialize(component));
}
public String getReason() {
return reason;
}
}

View File

@@ -0,0 +1,17 @@
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
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();
}
}

View File

@@ -1,4 +1,17 @@
package com.velocitypowered.proxy.protocol.packet;
public class LegacyPing {
import com.velocitypowered.proxy.protocol.MinecraftPacket;
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();
}
}

View File

@@ -1,6 +1,6 @@
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.proxy.data.ServerPing;
import com.velocitypowered.api.proxy.server.ServerPing;
import net.kyori.text.serializer.ComponentSerializers;
public class LegacyPingResponse {

View File

@@ -138,7 +138,9 @@ public class ScoreboardTeam implements MinecraftPacket {
}
this.flags = buf.readByte();
this.nameTagVisibility = ProtocolUtils.readString(buf, 32);
this.collisionRule = ProtocolUtils.readString(buf, 32);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) {
this.collisionRule = ProtocolUtils.readString(buf, 32);
}
this.color = protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ? buf.readByte() :
ProtocolUtils.readVarInt(buf);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) {
@@ -172,7 +174,9 @@ public class ScoreboardTeam implements MinecraftPacket {
}
buf.writeByte(flags);
ProtocolUtils.writeString(buf, nameTagVisibility);
ProtocolUtils.writeString(buf, collisionRule);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) {
ProtocolUtils.writeString(buf, collisionRule);
}
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) {
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, prefix);

View File

@@ -0,0 +1,102 @@
package com.velocitypowered.proxy.protocol.packet;
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_13;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_9;
public class TabCompleteRequest implements MinecraftPacket {
private int transactionId;
private String command;
private boolean assumeCommand;
private boolean hasPosition;
private long position;
public int getTransactionId() {
return transactionId;
}
public void setTransactionId(int transactionId) {
this.transactionId = transactionId;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public boolean isAssumeCommand() {
return assumeCommand;
}
public void setAssumeCommand(boolean assumeCommand) {
this.assumeCommand = assumeCommand;
}
public boolean isHasPosition() {
return hasPosition;
}
public void setHasPosition(boolean hasPosition) {
this.hasPosition = hasPosition;
}
public long getPosition() {
return position;
}
public void setPosition(long position) {
this.position = position;
}
@Override
public String toString() {
return "TabCompleteRequest{" +
"transactionId=" + transactionId +
", command='" + command + '\'' +
", assumeCommand=" + assumeCommand +
", hasPosition=" + hasPosition +
", position=" + position +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_13) {
this.transactionId = ProtocolUtils.readVarInt(buf);
this.command = ProtocolUtils.readString(buf);
} else {
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 void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_13) {
ProtocolUtils.writeVarInt(buf, transactionId);
ProtocolUtils.writeString(buf, command);
} else {
ProtocolUtils.writeString(buf, command);
if (protocolVersion >= MINECRAFT_1_9) {
buf.writeBoolean(assumeCommand);
}
buf.writeBoolean(hasPosition);
if (hasPosition) {
buf.writeLong(position);
}
}
}
}

View File

@@ -0,0 +1,128 @@
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.serializer.ComponentSerializers;
import java.util.ArrayList;
import java.util.List;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_13;
public class TabCompleteResponse implements MinecraftPacket {
private int transactionId;
private int start;
private int length;
private final List<Offer> offers = new ArrayList<>();
public int getTransactionId() {
return transactionId;
}
public void setTransactionId(int transactionId) {
this.transactionId = transactionId;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public List<Offer> getOffers() {
return offers;
}
@Override
public String toString() {
return "TabCompleteResponse{" +
"transactionId=" + transactionId +
", start=" + start +
", length=" + length +
", offers=" + offers +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_13) {
this.transactionId = ProtocolUtils.readVarInt(buf);
this.start = ProtocolUtils.readVarInt(buf);
this.length = ProtocolUtils.readVarInt(buf);
int offersAvailable = ProtocolUtils.readVarInt(buf);
for (int i = 0; i < offersAvailable; i++) {
String entry = ProtocolUtils.readString(buf);
Component component = buf.readBoolean() ? ComponentSerializers.JSON.deserialize(ProtocolUtils.readString(buf)) :
null;
offers.add(new Offer(entry, component));
}
} else {
int offersAvailable = ProtocolUtils.readVarInt(buf);
for (int i = 0; i < offersAvailable; i++) {
offers.add(new Offer(ProtocolUtils.readString(buf), null));
}
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_13) {
ProtocolUtils.writeVarInt(buf, transactionId);
ProtocolUtils.writeVarInt(buf, start);
ProtocolUtils.writeVarInt(buf, length);
ProtocolUtils.writeVarInt(buf, offers.size());
for (Offer offer : offers) {
ProtocolUtils.writeString(buf, offer.entry);
buf.writeBoolean(offer.tooltip != null);
if (offer.tooltip != null) {
ProtocolUtils.writeString(buf, ComponentSerializers.JSON.serialize(offer.tooltip));
}
}
} else {
ProtocolUtils.writeVarInt(buf, offers.size());
for (Offer offer : offers) {
ProtocolUtils.writeString(buf, offer.entry);
}
}
}
public static class Offer {
private final String entry;
private final Component tooltip;
public Offer(String entry, Component tooltip) {
this.entry = entry;
this.tooltip = tooltip;
}
public String getEntry() {
return entry;
}
public Component getTooltip() {
return tooltip;
}
@Override
public String toString() {
return "Offer{" +
"entry='" + entry + '\'' +
", tooltip=" + tooltip +
'}';
}
}
}

View File

@@ -5,7 +5,7 @@ import io.netty.buffer.ByteBuf;
/**
* Represents a protocol-specific entity ID remapper for certain Minecraft packets. This is mostly required to support
* old versions of Minecraft. For Minecraft 1.9 clients and above, Velocity can use a more efficient method based on
* old versions of Minecraft. For Minecraft 1.8 clients and above, Velocity can use a more efficient method based on
* sending JoinGame packets multiple times.
*/
public interface EntityIdRemapper {

View File

@@ -0,0 +1,18 @@
package com.velocitypowered.proxy.protocol.util;
import com.google.gson.*;
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) throws JsonParseException {
return new Favicon(json.getAsString());
}
@Override
public JsonElement serialize(Favicon src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getBase64Url());
}
}

View File

@@ -0,0 +1,7 @@
package com.velocitypowered.proxy.scheduler;
public interface Sleeper {
void sleep(long ms) throws InterruptedException;
Sleeper SYSTEM = Thread::sleep;
}

View File

@@ -0,0 +1,177 @@
package com.velocitypowered.proxy.scheduler;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class VelocityScheduler implements Scheduler {
private final PluginManager pluginManager;
private final ExecutorService taskService;
private final Sleeper sleeper;
private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedListMultimap(
Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
public VelocityScheduler(PluginManager pluginManager, Sleeper sleeper) {
this.pluginManager = pluginManager;
this.sleeper = sleeper;
this.taskService = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setDaemon(true)
.setNameFormat("Velocity Task Scheduler - #%d").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 {
for (ScheduledTask task : ImmutableList.copyOf(tasksByPlugin.values())) {
task.cancel();
}
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 delay(int time, TimeUnit unit) {
this.delay = unit.toMillis(time);
return this;
}
@Override
public TaskBuilder repeat(int 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);
taskService.execute(task);
tasksByPlugin.put(plugin, task);
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 volatile TaskStatus status;
private Thread taskThread;
private VelocityTask(Object plugin, Runnable runnable, long delay, long repeat) {
this.plugin = plugin;
this.runnable = runnable;
this.delay = delay;
this.repeat = repeat;
this.status = TaskStatus.SCHEDULED;
}
@Override
public Object plugin() {
return plugin;
}
@Override
public TaskStatus status() {
return status;
}
@Override
public void cancel() {
if (status == TaskStatus.SCHEDULED) {
status = TaskStatus.CANCELLED;
if (taskThread != null) {
taskThread.interrupt();
}
}
}
@Override
public void run() {
taskThread = Thread.currentThread();
if (delay > 0) {
try {
sleeper.sleep(delay);
} catch (InterruptedException e) {
if (status == TaskStatus.CANCELLED) {
onFinish();
return;
}
}
}
while (status != TaskStatus.CANCELLED) {
try {
runnable.run();
} catch (Exception e) {
Log.logger.error("Exception in task {} by plugin {}", runnable, plugin);
}
if (repeat > 0) {
try {
sleeper.sleep(repeat);
} catch (InterruptedException e) {
if (status == TaskStatus.CANCELLED) {
break;
}
}
} else {
status = TaskStatus.FINISHED;
break;
}
}
onFinish();
}
private void onFinish() {
tasksByPlugin.remove(plugin, this);
}
}
private static class Log {
private static final Logger logger = LogManager.getLogger(VelocityTask.class);
}
}

View File

@@ -0,0 +1,47 @@
package com.velocitypowered.proxy.util;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.net.InetAddress;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class Ratelimiter {
private final Cache<InetAddress, Long> expiringCache;
private final long timeoutNanos;
public Ratelimiter(long timeoutMs) {
this(timeoutMs, Ticker.systemTicker());
}
@VisibleForTesting
Ratelimiter(long timeoutMs, Ticker ticker) {
if (timeoutMs == 0) {
this.timeoutNanos = timeoutMs;
this.expiringCache = null;
} else {
this.timeoutNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMs);
this.expiringCache = CacheBuilder.newBuilder()
.ticker(ticker)
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.expireAfterWrite(timeoutMs, TimeUnit.MILLISECONDS)
.build();
}
}
public boolean attempt(InetAddress address) {
if (timeoutNanos == 0) return true;
long expectedNewValue = System.nanoTime() + timeoutNanos;
long last;
try {
last = expiringCache.get(address, () -> expectedNewValue);
} catch (ExecutionException e) {
// It should be impossible for this to fail.
throw new AssertionError(e);
}
return expectedNewValue == last;
}
}

View File

@@ -2,27 +2,55 @@ package com.velocitypowered.proxy.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.api.proxy.server.ServerInfo;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ServerMap {
private final Map<String, ServerInfo> servers = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public Optional<ServerInfo> getServer(String name) {
Preconditions.checkNotNull(name, "name");
return Optional.ofNullable(servers.get(name.toLowerCase()));
public Optional<ServerInfo> getServer(String server) {
Preconditions.checkNotNull(server, "server");
String lowerName = server.toLowerCase(Locale.US);
lock.readLock().lock();
try {
return Optional.ofNullable(servers.get(lowerName));
} finally {
lock.readLock().unlock();
}
}
public Collection<ServerInfo> getAllServers() {
return ImmutableList.copyOf(servers.values());
lock.readLock().lock();
try {
return ImmutableList.copyOf(servers.values());
} finally {
lock.readLock().unlock();
}
}
public void register(ServerInfo info) {
Preconditions.checkNotNull(info, "info");
servers.put(info.getName(), info);
public void register(ServerInfo server) {
Preconditions.checkNotNull(server, "server");
String lowerName = server.getName().toLowerCase(Locale.US);
lock.writeLock().lock();
try {
Preconditions.checkArgument(servers.putIfAbsent(lowerName, server) == null, "Server with name %s already registered", server.getName());
} finally {
lock.writeLock().unlock();
}
}
public void unregister(ServerInfo server) {
Preconditions.checkNotNull(server, "server");
String lowerName = server.getName().toLowerCase(Locale.US);
lock.writeLock().lock();
try {
Preconditions.checkArgument(servers.remove(lowerName, server), "Server with this name is not registered!");
} finally {
lock.writeLock().unlock();
}
}
}

View File

@@ -1,29 +0,0 @@
package com.velocitypowered.proxy.util;
import com.google.common.base.Preconditions;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.UUID;
public enum UuidUtils {
;
public static UUID fromUndashed(final String string) {
Objects.requireNonNull(string, "string");
Preconditions.checkArgument(string.length() == 32, "Length is incorrect");
return new UUID(
Long.parseUnsignedLong(string.substring(0, 16), 16),
Long.parseUnsignedLong(string.substring(16), 16)
);
}
public static String toUndashed(final UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid");
return Long.toUnsignedString(uuid.getMostSignificantBits(), 16) + Long.toUnsignedString(uuid.getLeastSignificantBits(), 16);
}
public static UUID generateOfflinePlayerUuid(String username) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -0,0 +1,45 @@
package com.velocitypowered.proxy.util.concurrency;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.MapMaker;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
/**
* A {@link ThreadFactory} that records the threads it has created. Once a thread terminates, it is automatically removed
* from the recorder.
*/
public class RecordingThreadFactory implements ThreadFactory {
private final ThreadFactory backing;
private final Set<Thread> threads = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap());
public RecordingThreadFactory(ThreadFactory backing) {
this.backing = Preconditions.checkNotNull(backing, "backing");
}
@Override
public Thread newThread(Runnable runnable) {
Preconditions.checkNotNull(runnable, "runnable");
return backing.newThread(() -> {
threads.add(Thread.currentThread());
try {
runnable.run();
} finally {
threads.remove(Thread.currentThread());
}
});
}
public boolean currentlyInFactory() {
return threads.contains(Thread.currentThread());
}
@VisibleForTesting
int size() {
return threads.size();
}
}

View File

@@ -0,0 +1 @@
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

View File

@@ -1,12 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
</Console>
<TerminalConsole name="TerminalConsole">
<PatternLayout>
<LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level] [%logger]: %minecraftFormatting{%msg}%n%xEx}">
<!-- Velocity doesn't need a prefix -->
<PatternMatch key="com.velocitypowered."
pattern="%highlightError{[%d{HH:mm:ss} %level]: %minecraftFormatting{%msg}%n%xEx}"/>
</LoggerNamePatternSelector>
</PatternLayout>
</TerminalConsole>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"
immediateFlush="false">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %minecraftFormatting{%msg}{strip}%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
@@ -16,7 +22,7 @@
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="TerminalConsole"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>

View File

@@ -0,0 +1,55 @@
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25577.
bind = "0.0.0.0:25577"
# What should be the MOTD? Legacy color codes and JSON are accepted.
motd = "&3A Velocity Server"
# What should we display for the maximum number of players? (Velocity does not support a cap
# on the number of players online.)
show-max-players = 500
# Should we authenticate players with Mojang? By default, this is on.
online-mode = true
# Should we forward IP addresses and other data to backend servers?
# Available options:
# - "none": No forwarding will be done. All players will appear to be connecting from the proxy
# and will have offline-mode UUIDs.
# - "legacy": Forward player IPs and UUIDs in BungeeCord-compatible fashion. Use this if you run
# servers using Minecraft 1.12 or lower.
# - "modern": Forward player IPs and UUIDs as part of the login process using Velocity's native
# forwarding. Only applicable for Minecraft 1.13 or higher.
player-info-forwarding = "modern"
# If you are using modern IP forwarding, configure an unique secret here.
player-info-forwarding-secret = "5up3r53cr3t"
[servers]
# Configure your servers here.
lobby = "127.0.0.1:30066"
factions = "127.0.0.1:30067"
minigames = "127.0.0.1:30068"
# In what order we should try servers when a player logs in or is kicked from a server.
try = [
"lobby"
]
[advanced]
# How large a Minecraft packet has to be before we compress it. Setting this to zero will compress all packets, and
# setting it to -1 will disable compression entirely.
compression-threshold = 1024
# How much compression should be done (from 0-9). The default is -1, which uses zlib's default level of 6.
compression-level = -1
# How fast (in miliseconds) are clients allowed to connect after the last connection? Default: 3000
# Disable by setting to 0
login-ratelimit = 3000
[query]
# Whether to enable responding to GameSpy 4 query responses or not
enabled = false
# If query responding is enabled, on what port should query response listener listen on?
port = 25577

View File

@@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*;
class PacketRegistryTest {
private StateRegistry.PacketRegistry setupRegistry() {
StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry(ProtocolConstants.Direction.CLIENTBOUND, StateRegistry.HANDSHAKE);
registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12));
registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, false));
return registry;
}
@@ -44,8 +44,8 @@ class PacketRegistryTest {
@Test
void registrySuppliesCorrectPacketsByProtocol() {
StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry(ProtocolConstants.Direction.CLIENTBOUND, StateRegistry.HANDSHAKE);
registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12),
new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12_1));
registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, false),
new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12_1, false));
assertEquals(Handshake.class, registry.getVersion(MINECRAFT_1_12).createPacket(0x00).getClass());
assertEquals(Handshake.class, registry.getVersion(MINECRAFT_1_12_1).createPacket(0x01).getClass());
assertEquals(Handshake.class, registry.getVersion(MINECRAFT_1_12_2).createPacket(0x01).getClass());

View File

@@ -0,0 +1,51 @@
package com.velocitypowered.proxy.scheduler;
import com.velocitypowered.api.scheduler.ScheduledTask;
import com.velocitypowered.api.scheduler.TaskStatus;
import com.velocitypowered.proxy.testutil.FakePluginManager;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
class VelocitySchedulerTest {
// TODO: The timings here will be inaccurate on slow systems. Need to find a testing-friendly replacement for Thread.sleep()
@Test
void buildTask() throws Exception {
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
CountDownLatch latch = new CountDownLatch(1);
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, latch::countDown).schedule();
latch.await();
assertEquals(TaskStatus.FINISHED, task.status());
}
@Test
void cancelWorks() throws Exception {
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
AtomicInteger i = new AtomicInteger(3);
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, i::decrementAndGet)
.delay(100, TimeUnit.SECONDS)
.schedule();
task.cancel();
Thread.sleep(200);
assertEquals(3, i.get());
assertEquals(TaskStatus.CANCELLED, task.status());
}
@Test
void repeatTaskWorks() throws Exception {
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
CountDownLatch latch = new CountDownLatch(3);
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, latch::countDown)
.delay(100, TimeUnit.MILLISECONDS)
.repeat(100, TimeUnit.MILLISECONDS)
.schedule();
latch.await();
task.cancel();
}
}

View File

@@ -0,0 +1,77 @@
package com.velocitypowered.proxy.testutil;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginManager;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
public class FakePluginManager implements PluginManager {
public static final Object PLUGIN_A = new Object();
public static final Object PLUGIN_B = new Object();
public static final PluginContainer PC_A = new FakePluginContainer("a", PLUGIN_A);
public static final PluginContainer PC_B = new FakePluginContainer("b", PLUGIN_B);
@Override
public @NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance) {
if (instance == PLUGIN_A) {
return Optional.of(PC_A);
} else if (instance == PLUGIN_B) {
return Optional.of(PC_B);
} else {
return Optional.empty();
}
}
@Override
public @NonNull Optional<PluginContainer> getPlugin(@NonNull String id) {
switch (id) {
case "a":
return Optional.of(PC_A);
case "b":
return Optional.of(PC_B);
default:
return Optional.empty();
}
}
@Override
public @NonNull Collection<PluginContainer> getPlugins() {
return ImmutableList.of(PC_A, PC_B);
}
@Override
public boolean isLoaded(@NonNull String id) {
return id.equals("a") || id.equals("b");
}
@Override
public void addToClasspath(@NonNull Object plugin, @NonNull Path path) {
throw new UnsupportedOperationException();
}
private static class FakePluginContainer implements PluginContainer {
private final String id;
private final Object instance;
private FakePluginContainer(String id, Object instance) {
this.id = id;
this.instance = instance;
}
@Override
public @NonNull PluginDescription getDescription() {
return () -> id;
}
@Override
public Optional<?> getInstance() {
return Optional.of(instance);
}
}
}

View File

@@ -0,0 +1,38 @@
package com.velocitypowered.proxy.util;
import com.google.common.base.Ticker;
import org.junit.jupiter.api.Test;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.jupiter.api.Assertions.*;
class RatelimiterTest {
@Test
void attemptZero() {
Ratelimiter noRatelimiter = new Ratelimiter(0);
assertTrue(noRatelimiter.attempt(InetAddress.getLoopbackAddress()));
assertTrue(noRatelimiter.attempt(InetAddress.getLoopbackAddress()));
}
@Test
void attemptOne() {
long base = System.nanoTime();
AtomicLong extra = new AtomicLong();
Ticker testTicker = new Ticker() {
@Override
public long read() {
return base + extra.get();
}
};
Ratelimiter ratelimiter = new Ratelimiter(1000, testTicker);
assertTrue(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
assertFalse(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
extra.addAndGet(TimeUnit.SECONDS.toNanos(2));
assertTrue(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
}
}

View File

@@ -0,0 +1,35 @@
package com.velocitypowered.proxy.util;
import com.velocitypowered.api.proxy.server.ServerInfo;
import org.junit.jupiter.api.Test;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
class ServerMapTest {
private static final InetSocketAddress TEST_ADDRESS = new InetSocketAddress(InetAddress.getLoopbackAddress(), 25565);
@Test
void respectsCaseInsensitivity() {
ServerMap map = new ServerMap();
ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS);
map.register(info);
assertEquals(Optional.of(info), map.getServer("TestServer"));
assertEquals(Optional.of(info), map.getServer("testserver"));
assertEquals(Optional.of(info), map.getServer("TESTSERVER"));
}
@Test
void rejectsRepeatedRegisterAttempts() {
ServerMap map = new ServerMap();
ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS);
map.register(info);
ServerInfo willReject = new ServerInfo("TESTSERVER", TEST_ADDRESS);
assertThrows(IllegalArgumentException.class, () -> map.register(willReject));
}
}

View File

@@ -1,5 +1,6 @@
package com.velocitypowered.proxy.util;
import com.velocitypowered.api.util.UuidUtils;
import org.junit.jupiter.api.Test;
import java.util.UUID;

View File

@@ -0,0 +1,36 @@
package com.velocitypowered.proxy.util.concurrency;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import static org.junit.jupiter.api.Assertions.*;
class RecordingThreadFactoryTest {
@Test
void newThread() throws Exception {
RecordingThreadFactory factory = new RecordingThreadFactory(Executors.defaultThreadFactory());
CountDownLatch started = new CountDownLatch(1);
CountDownLatch endThread = new CountDownLatch(1);
factory.newThread(() -> {
started.countDown();
assertTrue(factory.currentlyInFactory());
assertEquals(1, factory.size());
try {
endThread.await();
} catch (InterruptedException e) {
fail(e);
}
}).start();
started.await();
assertFalse(factory.currentlyInFactory());
assertEquals(1, factory.size());
endThread.countDown();
// Wait a little bit to ensure the thread got shut down
Thread.sleep(10);
assertEquals(0, factory.size());
}
}