diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyReloadEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyReloadEvent.java new file mode 100644 index 000000000..ec1ed1ad0 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyReloadEvent.java @@ -0,0 +1,12 @@ +package com.velocitypowered.api.event.proxy; + +/** + * This event is fired when the proxy is reloaded by the user using {@code /velocity reload}. + */ +public class ProxyReloadEvent { + + @Override + public String toString() { + return "ProxyReloadEvent"; + } +} diff --git a/build.gradle b/build.gradle index ec2d00c2b..6a86013d9 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ allprojects { junitVersion = '5.3.0-M1' slf4jVersion = '1.7.25' log4jVersion = '2.11.1' - nettyVersion = '4.1.30.Final' + nettyVersion = '4.1.32.Final' guavaVersion = '25.1-jre' checkerFrameworkVersion = '2.5.6' diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 339199141..821e0e4ea 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -7,6 +7,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyReloadEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginManager; @@ -40,17 +41,20 @@ import com.velocitypowered.proxy.util.VelocityChannelRegistrar; import com.velocitypowered.proxy.util.ratelimit.Ratelimiter; import com.velocitypowered.proxy.util.ratelimit.Ratelimiters; import io.netty.bootstrap.Bootstrap; +import java.io.IOException; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyPair; +import java.util.ArrayList; import java.util.Collection; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import net.kyori.text.Component; import net.kyori.text.TextComponent; @@ -70,26 +74,32 @@ public class VelocityServer implements ProxyServer { .registerTypeHierarchyAdapter(GameProfile.class, new GameProfileSerializer()) .create(); + private final ConnectionManager cm; private final ProxyOptions options; - private @MonotonicNonNull ConnectionManager cm; private @MonotonicNonNull VelocityConfiguration configuration; private @MonotonicNonNull NettyHttpClient httpClient; private @MonotonicNonNull KeyPair serverKeyPair; - private @MonotonicNonNull ServerMap servers; + private final ServerMap servers; private final VelocityCommandManager commandManager = new VelocityCommandManager(); private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false); private boolean shutdown = false; - private @MonotonicNonNull VelocityPluginManager pluginManager; + private final VelocityPluginManager pluginManager; private final Map connectionsByUuid = new ConcurrentHashMap<>(); private final Map connectionsByName = new ConcurrentHashMap<>(); - private @MonotonicNonNull VelocityConsole console; + private final VelocityConsole console; private @MonotonicNonNull Ratelimiter ipAttemptLimiter; - private @MonotonicNonNull VelocityEventManager eventManager; - private @MonotonicNonNull VelocityScheduler scheduler; + private final VelocityEventManager eventManager; + private final VelocityScheduler scheduler; private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar(); - public VelocityServer(final ProxyOptions options) { + VelocityServer(final ProxyOptions options) { + pluginManager = new VelocityPluginManager(this); + eventManager = new VelocityEventManager(pluginManager); + scheduler = new VelocityScheduler(pluginManager); + console = new VelocityConsole(this); + cm = new ConnectionManager(this); + servers = new ServerMap(this); this.options = options; } @@ -138,12 +148,6 @@ public class VelocityServer implements ProxyServer { logger.info("Booting up {} {}...", getVersion().getName(), getVersion().getVersion()); serverKeyPair = EncryptionUtils.createRsaKeyPair(1024); - pluginManager = new VelocityPluginManager(this); - eventManager = new VelocityEventManager(pluginManager); - scheduler = new VelocityScheduler(pluginManager); - console = new VelocityConsole(this); - cm = new ConnectionManager(this); - servers = new ServerMap(this); cm.logChannelInformation(); @@ -233,9 +237,6 @@ public class VelocityServer implements ProxyServer { } public Bootstrap initializeGenericBootstrap() { - if (cm == null) { - throw new IllegalStateException("Server did not initialize properly."); - } return this.cm.createWorker(); } @@ -243,6 +244,87 @@ public class VelocityServer implements ProxyServer { return shutdown; } + public boolean reloadConfiguration() throws IOException { + Path configPath = Paths.get("velocity.toml"); + VelocityConfiguration newConfiguration = VelocityConfiguration.read(configPath); + + if (!newConfiguration.validate()) { + return false; + } + + // Re-register servers. If a server is being replaced, make sure to note what players need to + // move back to a fallback server. + Collection evacuate = new ArrayList<>(); + for (Map.Entry entry : newConfiguration.getServers().entrySet()) { + ServerInfo newInfo = + new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue())); + Optional rs = servers.getServer(entry.getKey()); + if (!rs.isPresent()) { + servers.register(newInfo); + } else if (!rs.get().getServerInfo().equals(newInfo)) { + for (Player player : rs.get().getPlayersConnected()) { + if (!(player instanceof ConnectedPlayer)) { + throw new IllegalStateException("ConnectedPlayer not found for player " + player + + " in server " + rs.get().getServerInfo().getName()); + } + evacuate.add((ConnectedPlayer) player); + } + servers.unregister(rs.get().getServerInfo()); + servers.register(newInfo); + } + } + + // If we had any players to evacuate, let's move them now. Wait until they are all moved off. + if (!evacuate.isEmpty()) { + CountDownLatch latch = new CountDownLatch(evacuate.size()); + for (ConnectedPlayer player : evacuate) { + Optional next = player.getNextServerToTry(); + if (next.isPresent()) { + player.createConnectionRequest(next.get()).connectWithIndication() + .whenComplete((success, ex) -> { + if (ex != null || success == null || !success) { + player.disconnect(TextComponent.of("Your server has been changed, but we could " + + "not move you to any fallback servers.")); + } + latch.countDown(); + }); + } else { + latch.countDown(); + player.disconnect(TextComponent.of("Your server has been changed, but we could " + + "not move you to any fallback servers.")); + } + } + try { + latch.await(); + } catch (InterruptedException e) { + logger.error("Interrupted whilst moving players", e); + Thread.currentThread().interrupt(); + } + } + + // If we have a new bind address, bind to it + if (!configuration.getBind().equals(newConfiguration.getBind())) { + this.cm.bind(newConfiguration.getBind()); + this.cm.close(configuration.getBind()); + } + + if (configuration.isQueryEnabled() && (!newConfiguration.isQueryEnabled() + || newConfiguration.getQueryPort() != configuration.getQueryPort())) { + this.cm.close(new InetSocketAddress( + configuration.getBind().getHostString(), configuration.getQueryPort())); + } + + if (newConfiguration.isQueryEnabled()) { + this.cm.queryBind(newConfiguration.getBind().getHostString(), + newConfiguration.getQueryPort()); + } + + ipAttemptLimiter = Ratelimiters.createWithMilliseconds(newConfiguration.getLoginRatelimit()); + this.configuration = newConfiguration; + eventManager.fireAndForget(new ProxyReloadEvent()); + return true; + } + public void shutdown(boolean explicitExit) { if (eventManager == null || pluginManager == null || cm == null || scheduler == null) { throw new AssertionError(); @@ -340,66 +422,41 @@ public class VelocityServer implements ProxyServer { @Override public Optional getServer(String name) { - Preconditions.checkNotNull(name, "name"); - if (servers == null) { - throw new IllegalStateException("Server did not initialize properly."); - } return servers.getServer(name); } @Override public Collection getAllServers() { - if (servers == null) { - throw new IllegalStateException("Server did not initialize properly."); - } return servers.getAllServers(); } @Override public RegisteredServer registerServer(ServerInfo server) { - if (servers == null) { - throw new IllegalStateException("Server did not initialize properly."); - } return servers.register(server); } @Override public void unregisterServer(ServerInfo server) { - if (servers == null) { - throw new IllegalStateException("Server did not initialize properly."); - } servers.unregister(server); } @Override public VelocityConsole getConsoleCommandSource() { - if (console == null) { - throw new IllegalStateException("Server did not initialize properly."); - } return console; } @Override public PluginManager getPluginManager() { - if (pluginManager == null) { - throw new IllegalStateException("Server did not initialize properly."); - } return pluginManager; } @Override public EventManager getEventManager() { - if (eventManager == null) { - throw new IllegalStateException("Server did not initialize properly."); - } return eventManager; } @Override public VelocityScheduler getScheduler() { - if (scheduler == null) { - throw new IllegalStateException("Server did not initialize properly."); - } return scheduler; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java index a6449a5e4..57baa0106 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java @@ -10,6 +10,8 @@ import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.util.ProxyVersion; +import com.velocitypowered.proxy.VelocityServer; +import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -22,6 +24,8 @@ import net.kyori.text.event.HoverEvent; import net.kyori.text.event.HoverEvent.Action; import net.kyori.text.format.TextColor; import net.kyori.text.format.TextDecoration; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; public class VelocityCommand implements Command { @@ -32,10 +36,11 @@ public class VelocityCommand implements Command { * Initializes the command object for /velocity. * @param server the Velocity server */ - public VelocityCommand(ProxyServer server) { + public VelocityCommand(VelocityServer server) { this.subcommands = ImmutableMap.builder() .put("version", new Info(server)) .put("plugins", new Plugins(server)) + .put("reload", new Reload(server)) .build(); } @@ -103,6 +108,39 @@ public class VelocityCommand implements Command { return command.hasPermission(source, actualArgs); } + private static class Reload implements Command { + + private static final Logger logger = LogManager.getLogger(Reload.class); + private final VelocityServer server; + + private Reload(VelocityServer server) { + this.server = server; + } + + @Override + public void execute(CommandSource source, String @NonNull [] args) { + try { + if (server.reloadConfiguration()) { + source.sendMessage(TextComponent.of("Configuration reloaded.", TextColor.GREEN)); + } else { + source.sendMessage(TextComponent.of( + "Unable to reload your configuration. Check the console for more details.", + TextColor.RED)); + } + } catch (Exception e) { + logger.error("Unable to reload configuration", e); + source.sendMessage(TextComponent.of( + "Unable to reload your configuration. Check the console for more details.", + TextColor.RED)); + } + } + + @Override + public boolean hasPermission(CommandSource source, String @NonNull [] args) { + return source.getPermissionValue("velocity.command.reload") == Tristate.TRUE; + } + } + private static class Info implements Command { private final ProxyServer server; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index ea71285eb..9a7e32e8f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -383,7 +383,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } } - Optional getNextServerToTry() { + public Optional getNextServerToTry() { if (serversToTry == null) { String virtualHostStr = getVirtualHost().map(InetSocketAddress::getHostString).orElse(""); serversToTry = server.getConfiguration().getForcedHosts() diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index 92d5a657d..a0ff135c1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -1,5 +1,6 @@ package com.velocitypowered.proxy.network; +import com.google.common.base.Preconditions; import com.velocitypowered.natives.util.Natives; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler; @@ -11,7 +12,9 @@ import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.WriteBufferWaterMark; import java.net.InetSocketAddress; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -21,7 +24,7 @@ public final class ConnectionManager { private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 16, 1 << 18); private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class); - private final Set endpoints = new HashSet<>(); + private final Map endpoints = new HashMap<>(); private final TransportType transportType; private final EventLoopGroup bossGroup; private final EventLoopGroup workerGroup; @@ -55,7 +58,7 @@ public final class ConnectionManager { .addListener((ChannelFutureListener) future -> { final Channel channel = future.channel(); if (future.isSuccess()) { - this.endpoints.add(channel); + this.endpoints.put(address, channel); LOGGER.info("Listening on {}", channel.localAddress()); } else { LOGGER.error("Can't bind to {}", address, future.cause()); @@ -64,16 +67,17 @@ public final class ConnectionManager { } public void queryBind(final String hostname, final int port) { + InetSocketAddress address = new InetSocketAddress(hostname, port); final Bootstrap bootstrap = new Bootstrap() .channel(this.transportType.datagramChannelClass) .group(this.workerGroup) .handler(new GS4QueryHandler(this.server)) - .localAddress(hostname, port); + .localAddress(address); bootstrap.bind() .addListener((ChannelFutureListener) future -> { final Channel channel = future.channel(); if (future.isSuccess()) { - this.endpoints.add(channel); + this.endpoints.put(address, channel); LOGGER.info("Listening for GS4 query on {}", channel.localAddress()); } else { LOGGER.error("Can't bind to {}", bootstrap.config().localAddress(), future.cause()); @@ -90,8 +94,15 @@ public final class ConnectionManager { this.server.getConfiguration().getConnectTimeout()); } + public void close(InetSocketAddress oldBind) { + Channel serverChannel = endpoints.remove(oldBind); + Preconditions.checkState(serverChannel != null, "Endpoint %s not registered", oldBind); + LOGGER.info("Closing endpoint {}", serverChannel.localAddress()); + serverChannel.close().syncUninterruptibly(); + } + public void shutdown() { - for (final Channel endpoint : this.endpoints) { + for (final Channel endpoint : this.endpoints.values()) { try { LOGGER.info("Closing endpoint {}", endpoint.localAddress()); endpoint.close().sync();