Merge branch 'master' into native-crypto
# Conflicts: # native/src/main/java/com/velocitypowered/natives/util/Natives.java
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
package com.velocitypowered.proxy.config;
|
||||
|
||||
public enum IPForwardingMode {
|
||||
public enum PlayerInfoForwarding {
|
||||
NONE,
|
||||
LEGACY,
|
||||
MODERN
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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.
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.velocitypowered.network;
|
||||
package com.velocitypowered.proxy.network;
|
||||
|
||||
public interface Connections {
|
||||
String CIPHER_DECODER = "cipher-decoder";
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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) +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
public interface Sleeper {
|
||||
void sleep(long ms) throws InterruptedException;
|
||||
|
||||
Sleeper SYSTEM = Thread::sleep;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
1
proxy/src/main/resources/log4j2.component.properties
Normal file
1
proxy/src/main/resources/log4j2.component.properties
Normal file
@@ -0,0 +1 @@
|
||||
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
|
@@ -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>
|
||||
|
55
proxy/src/main/resources/velocity.toml
Normal file
55
proxy/src/main/resources/velocity.toml
Normal 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
|
@@ -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());
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user