diff --git a/.gitignore b/.gitignore index 1fe58326e..00b45a827 100644 --- a/.gitignore +++ b/.gitignore @@ -120,4 +120,5 @@ gradle-app.setting # End of https://www.gitignore.io/api/java,gradle,intellij # Other trash -logs/ \ No newline at end of file +logs/ +velocity.toml \ No newline at end of file diff --git a/build.gradle b/build.gradle index 04ac38b7f..9f05623cf 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ dependencies { compile 'net.kyori:text:1.12-1.5.0' compile 'org.apache.logging.log4j:log4j-api:2.11.0' compile 'org.apache.logging.log4j:log4j-core:2.11.0' + compile 'com.moandjiezana.toml:toml4j:0.7.2' testCompile 'org.junit.jupiter:junit-jupiter-api:5.3.0-M1' testCompile 'org.junit.jupiter:junit-jupiter-engine:5.3.0-M1' } diff --git a/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 93ad10196..ec8fc430b 100644 --- a/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -3,6 +3,7 @@ package com.velocitypowered.proxy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.velocitypowered.network.ConnectionManager; +import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.http.NettyHttpClient; import com.velocitypowered.proxy.util.EncryptionUtils; import io.netty.bootstrap.Bootstrap; @@ -11,7 +12,9 @@ import net.kyori.text.serializer.GsonComponentSerializer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.file.Paths; import java.security.KeyPair; public class VelocityServer { @@ -22,6 +25,7 @@ public class VelocityServer { .create(); private final ConnectionManager cm = new ConnectionManager(); + private VelocityConfiguration configuration; private NettyHttpClient httpClient; private KeyPair serverKeyPair; @@ -36,9 +40,23 @@ public class VelocityServer { return serverKeyPair; } + public VelocityConfiguration getConfiguration() { + return configuration; + } + public void start() { // Create a key pair logger.info("Booting up Velocity..."); + try { + configuration = VelocityConfiguration.read(Paths.get("velocity.toml")); + if (!configuration.validate()) { + logger.error("Your configuration is invalid. Velocity will refuse to start up until the errors are resolved."); + System.exit(1); + } + } catch (IOException e) { + logger.error("Unable to load your velocity.toml. The server will shut down.", e); + System.exit(1); + } serverKeyPair = EncryptionUtils.createRsaKeyPair(1024); httpClient = new NettyHttpClient(this); diff --git a/src/main/java/com/velocitypowered/proxy/config/IPForwardingMode.java b/src/main/java/com/velocitypowered/proxy/config/IPForwardingMode.java new file mode 100644 index 000000000..87d449c2e --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/config/IPForwardingMode.java @@ -0,0 +1,7 @@ +package com.velocitypowered.proxy.config; + +public enum IPForwardingMode { + NONE, + LEGACY, + MODERN +} diff --git a/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java new file mode 100644 index 000000000..d184cdfd7 --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -0,0 +1,163 @@ +package com.velocitypowered.proxy.config; + +import com.google.common.collect.ImmutableMap; +import com.moandjiezana.toml.Toml; +import com.velocitypowered.proxy.util.LegacyChatColorUtils; +import net.kyori.text.Component; +import net.kyori.text.serializer.ComponentSerializers; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class VelocityConfiguration { + private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class); + + private final String bind; + private final String motd; + private final int showMaxPlayers; + private final boolean onlineMode; + private final IPForwardingMode ipForwardingMode; + private final Map servers; + private final List attemptConnectionOrder; + + private Component motdAsComponent; + + private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode, + IPForwardingMode ipForwardingMode, Map servers, + List attemptConnectionOrder) { + this.bind = bind; + this.motd = motd; + this.showMaxPlayers = showMaxPlayers; + this.onlineMode = onlineMode; + this.ipForwardingMode = ipForwardingMode; + this.servers = servers; + this.attemptConnectionOrder = attemptConnectionOrder; + } + + public boolean validate() { + boolean valid = true; + + if (bind.isEmpty()) { + logger.error("'bind' option is empty."); + valid = false; + } + + if (!onlineMode) { + logger.info("Proxy is running in offline mode!"); + } + + switch (ipForwardingMode) { + case NONE: + logger.info("IP forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs."); + break; + case MODERN: + logger.warn("Modern IP forwarding is not currently implemented."); + break; + } + + if (servers.isEmpty()) { + logger.error("You have no servers configured. :("); + valid = false; + } else { + if (attemptConnectionOrder.isEmpty()) { + logger.error("No fallback servers are configured!"); + valid = false; + } + + for (String s : attemptConnectionOrder) { + if (!servers.containsKey(s)) { + logger.error("Fallback server " + s + " doesn't exist!"); + valid = false; + } + } + } + + return valid; + } + + public String getBind() { + return bind; + } + + public String getMotd() { + return motd; + } + + public Component getMotdComponent() { + if (motdAsComponent == null) { + if (motd.startsWith("{")) { + motdAsComponent = ComponentSerializers.JSON.deserialize(motd); + } else { + motdAsComponent = ComponentSerializers.LEGACY.deserialize(LegacyChatColorUtils.translate('&', motd)); + } + } + return motdAsComponent; + } + + public int getShowMaxPlayers() { + return showMaxPlayers; + } + + public boolean isOnlineMode() { + return onlineMode; + } + + public IPForwardingMode getIpForwardingMode() { + return ipForwardingMode; + } + + public Map getServers() { + return servers; + } + + public List getAttemptConnectionOrder() { + return attemptConnectionOrder; + } + + @Override + public String toString() { + return "VelocityConfiguration{" + + "bind='" + bind + '\'' + + ", motd='" + motd + '\'' + + ", showMaxPlayers=" + showMaxPlayers + + ", onlineMode=" + onlineMode + + ", ipForwardingMode=" + ipForwardingMode + + ", servers=" + servers + + ", attemptConnectionOrder=" + attemptConnectionOrder + + '}'; + } + + public static VelocityConfiguration read(Path path) throws IOException { + try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + Toml toml = new Toml().read(reader); + + Map servers = new HashMap<>(); + for (Map.Entry entry : toml.getTable("servers").entrySet()) { + if (entry.getValue() instanceof String) { + servers.put(entry.getKey(), (String) entry.getValue()); + } else { + if (!entry.getKey().equalsIgnoreCase("try")) { + throw new IllegalArgumentException("Server entry " + entry.getKey() + " is not a string!"); + } + } + } + + 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()), + ImmutableMap.copyOf(servers), + toml.getTable("servers").getList("try")); + } + } +} diff --git a/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index f8de5656b..633c859e2 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -97,6 +97,16 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { channel.writeAndFlush(msg, channel.voidPromise()); } + public void delayedWrite(Object msg) { + ensureOpen(); + channel.write(msg, channel.voidPromise()); + } + + public void flush() { + ensureOpen(); + channel.flush(); + } + public void closeWith(Object msg) { ensureOpen(); teardown(); diff --git a/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java b/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java index f314d81ac..f71223bec 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java +++ b/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java @@ -1,5 +1,6 @@ package com.velocitypowered.proxy.connection.backend; +import com.velocitypowered.proxy.config.IPForwardingMode; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; @@ -86,7 +87,11 @@ public class ServerConnection { Handshake handshake = new Handshake(); handshake.setNextStatus(2); // login handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion()); - handshake.setServerAddress(createBungeeForwardingAddress()); + if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.LEGACY) { + handshake.setServerAddress(createBungeeForwardingAddress()); + } else { + handshake.setServerAddress(serverInfo.getAddress().getHostString()); + } handshake.setPort(serverInfo.getAddress().getPort()); channel.write(handshake); diff --git a/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 969bd625e..601800189 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -95,10 +95,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // - 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. - player.getConnection().write(joinGame); + player.getConnection().delayedWrite(joinGame); int tempDim = joinGame.getDimension() == 0 ? -1 : 0; - player.getConnection().write(new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType())); - player.getConnection().write(new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType())); + 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())); + player.getConnection().flush(); currentDimension = joinGame.getDimension(); } } diff --git a/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java b/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index 04f226507..61085b5c7 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java +++ b/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java @@ -41,13 +41,15 @@ public class LoginSessionHandler implements MinecraftSessionHandler { if (packet instanceof ServerLogin) { this.login = (ServerLogin) packet; - // Request encryption. - EncryptionRequest request = generateRequest(); - this.verify = Arrays.copyOf(request.getVerifyToken(), 4); - inbound.write(request); - - // TODO: Online-mode checks - //handleSuccessfulLogin(); + if (VelocityServer.getServer().getConfiguration().isOnlineMode()) { + // Request encryption. + EncryptionRequest request = generateRequest(); + this.verify = Arrays.copyOf(request.getVerifyToken(), 4); + inbound.write(request); + } else { + // Offline-mode, don't try to request encryption. + handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername())); + } } if (packet instanceof EncryptionResponse) { diff --git a/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index 8d688b1d2..1cc6a9f00 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -4,6 +4,7 @@ import com.google.common.base.Preconditions; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packets.Ping; import com.velocitypowered.proxy.protocol.packets.StatusRequest; @@ -33,11 +34,13 @@ public class StatusSessionHandler implements MinecraftSessionHandler { return; } + VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration(); + // Status request ServerPing ping = new ServerPing( new ServerPing.Version(340, "1.12.2"), - new ServerPing.Players(0, 0), - TextComponent.of("test"), + new ServerPing.Players(0, configuration.getShowMaxPlayers()), + configuration.getMotdComponent(), null ); StatusResponse response = new StatusResponse(); diff --git a/src/main/java/com/velocitypowered/proxy/util/LegacyChatColorUtils.java b/src/main/java/com/velocitypowered/proxy/util/LegacyChatColorUtils.java new file mode 100644 index 000000000..8488002ea --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/util/LegacyChatColorUtils.java @@ -0,0 +1,56 @@ +package com.velocitypowered.proxy.util; + +import com.google.common.base.Preconditions; + +import java.util.regex.Pattern; + +/** + * Utilities for handling legacy Minecraft color codes. Generally, you should prefer JSON-based components, but for + * convenience Velocity provides these utilities. + */ +public enum LegacyChatColorUtils { + ; + + public static final char FORMAT_CHAR = '\u00a7'; + private static final Pattern CHAT_COLOR_MATCHER = Pattern.compile("(?i)" + Character.toString(FORMAT_CHAR) + "[0-9A-FL-OR]"); + + /** + * Translates a string with Minecraft color codes prefixed with a different character than the section symbol into + * a string that uses the section symbol. + * @param originalChar the char the color codes are prefixed by + * @param text the text to translate + * @return the translated text + */ + public static String translate(char originalChar, String text) { + Preconditions.checkNotNull(text, "text"); + char[] textChars = text.toCharArray(); + int foundSectionIdx = -1; + for (int i = 0; i < textChars.length; i++) { + char textChar = textChars[i]; + if (textChar == originalChar) { + foundSectionIdx = i; + continue; + } + + if (foundSectionIdx >= 0) { + textChar = Character.toLowerCase(textChar); + if ((textChar >= 'a' && textChar <= 'f') || (textChar >= '0' && textChar <= '9') || + (textChar >= 'l' && textChar <= 'o' || textChar == 'r')) { + textChars[foundSectionIdx] = FORMAT_CHAR; + } + foundSectionIdx = -1; + } + } + return new String(textChars); + } + + /** + * Removes all Minecraft color codes from the string. + * @param text the text to remove color codes from + * @return a new String without Minecraft color codes + */ + public static String removeFormatting(String text) { + Preconditions.checkNotNull(text, "text"); + return CHAT_COLOR_MATCHER.matcher(text).replaceAll(""); + } +} diff --git a/src/main/resources/velocity.toml b/src/main/resources/velocity.toml new file mode 100644 index 000000000..648f9e4a4 --- /dev/null +++ b/src/main/resources/velocity.toml @@ -0,0 +1,33 @@ +# 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. +ip-forwarding = "modern" + +[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" +] \ No newline at end of file diff --git a/src/test/java/com/velocitypowered/proxy/util/LegacyChatColorUtilsTest.java b/src/test/java/com/velocitypowered/proxy/util/LegacyChatColorUtilsTest.java new file mode 100644 index 000000000..04841bf78 --- /dev/null +++ b/src/test/java/com/velocitypowered/proxy/util/LegacyChatColorUtilsTest.java @@ -0,0 +1,61 @@ +package com.velocitypowered.proxy.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class LegacyChatColorUtilsTest { + private static final String NON_FORMATTED = "Velocity"; + private static final String FORMATTED = "\u00a7cVelocity"; + private static final String FORMATTED_MULTIPLE = "\u00a7c\u00a7lVelocity"; + private static final String FORMATTED_MULTIPLE_VARIED = "\u00a7c\u00a7lVelo\u00a7a\u00a7mcity"; + private static final String INVALID = "\u00a7gVelocity"; + private static final String RAW_SECTION = "\u00a7"; + + @Test + void removeFormattingNonFormatted() { + assertEquals(NON_FORMATTED, LegacyChatColorUtils.removeFormatting(NON_FORMATTED)); + } + + @Test + void removeFormattingFormatted() { + assertEquals(NON_FORMATTED, LegacyChatColorUtils.removeFormatting(FORMATTED)); + } + + @Test + void removeFormattingFormattedMultiple() { + assertEquals(NON_FORMATTED, LegacyChatColorUtils.removeFormatting(FORMATTED_MULTIPLE)); + } + + @Test + void removeFormattingFormattedMultipleVaried() { + assertEquals(NON_FORMATTED, LegacyChatColorUtils.removeFormatting(FORMATTED_MULTIPLE_VARIED)); + } + + @Test + void removeFormattingInvalidFormat() { + assertEquals(INVALID, LegacyChatColorUtils.removeFormatting(INVALID)); + } + + @Test + void removeFormattingRawSection() { + assertEquals(RAW_SECTION, LegacyChatColorUtils.removeFormatting(RAW_SECTION)); + } + + @Test + void translate() { + assertEquals(FORMATTED, LegacyChatColorUtils.translate('&', "&cVelocity")); + } + + @Test + void translateMultiple() { + assertEquals(FORMATTED_MULTIPLE, LegacyChatColorUtils.translate('&', "&c&lVelocity")); + assertEquals(FORMATTED_MULTIPLE_VARIED, LegacyChatColorUtils.translate('&', "&c&lVelo&a&mcity")); + } + + @Test + void translateDifferentChar() { + assertEquals(FORMATTED, LegacyChatColorUtils.translate('$', "$cVelocity")); + assertEquals(FORMATTED_MULTIPLE_VARIED, LegacyChatColorUtils.translate('$', "$c$lVelo$a$mcity")); + } +} \ No newline at end of file diff --git a/velocity.toml b/velocity.toml new file mode 100644 index 000000000..1c25e360b --- /dev/null +++ b/velocity.toml @@ -0,0 +1,32 @@ +# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25577. +bind = "0.0.0.0:26671" + +# 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. +ip-forwarding = "legacy" + +[servers] +# Configure your servers here. +lobby = "127.0.0.1:25565" +lobby2 = "127.0.0.1:25566" + +# In what order we should try servers when a player logs in or is kicked from a server. +try = [ + "lobby" +] \ No newline at end of file