Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved configuration migration #1111

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty"
netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.6.6"
nightconfig = "com.electronwill.night-config:toml:3.6.7"
slf4j = "org.slf4j:slf4j-api:2.0.7"
snakeyaml = "org.yaml:snakeyaml:1.33"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
import com.google.gson.annotations.Expose;
import com.velocitypowered.api.proxy.config.ProxyConfig;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.config.migration.ConfigurationMigration;
import com.velocitypowered.proxy.config.migration.ForwardingMigration;
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.util.AddressUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
Expand All @@ -42,8 +46,6 @@
import java.util.Optional;
import java.util.Random;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Expand All @@ -59,7 +61,7 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose
private String bind = "0.0.0.0:25577";
@Expose
private String motd = "&3A Velocity Server";
private String motd = "<aqua>A Velocity Server";
@Expose
private int showMaxPlayers = 500;
@Expose
Expand Down Expand Up @@ -353,7 +355,7 @@ public boolean isProxyProtocol() {
}

public boolean useTcpFastOpen() {
return advanced.tcpFastOpen;
return advanced.isTcpFastOpen();
}

public Metrics getMetrics() {
Expand Down Expand Up @@ -433,171 +435,115 @@ public static VelocityConfiguration read(Path path) throws IOException {
}

// Create the forwarding-secret file on first-time startup if it doesn't exist
Path defaultForwardingSecretPath = Path.of("forwarding.secret");
final Path defaultForwardingSecretPath = Path.of("forwarding.secret");
if (Files.notExists(path) && Files.notExists(defaultForwardingSecretPath)) {
Files.writeString(defaultForwardingSecretPath, generateRandomString(12));
}

boolean mustResave = false;
CommentedFileConfig config = CommentedFileConfig.builder(path)
.defaultData(defaultConfigLocation)
.autosave()
.preserveInsertionOrder()
.sync()
.build();
config.load();

// TODO: migrate this on Velocity Polymer
double configVersion;
try {
configVersion = Double.parseDouble(config.getOrElse("config-version", "1.0"));
} catch (NumberFormatException e) {
configVersion = 1.0;
}

// Whether or not this config version is older than 2.0 which uses the deprecated
// "forwarding-secret" parameter
boolean legacyConfig = configVersion < 2.0;

String forwardingSecretString;
byte[] forwardingSecret;

// Handle the previous (version 1.0) config
// There is duplicate/old code here in effort to make the future commit which abandons legacy
// config handling easier to implement. All that would be required is removing the if statement
// here and keeping the contents of the else block (with slight tidying).
if (legacyConfig) {
logger.warn(
"You are currently using a deprecated configuration version. The \"forwarding-secret\""
+ " parameter is a security hazard and was removed in config version 2.0."
+ " You should rename your current \"velocity.toml\" to something else to allow"
+ " Velocity to generate a config file for the new version. You may then configure "
+ " that file as you normally would. The only differences are the config-version "
+ "and \"forwarding-secret\" has been replaced by \"forwarding-secret-file\".");

// Default legacy handling
forwardingSecretString = System.getenv()
.getOrDefault("VELOCITY_FORWARDING_SECRET", config.get("forwarding-secret"));
if (forwardingSecretString == null || forwardingSecretString.isEmpty()) {
forwardingSecretString = generateRandomString(12);
config.set("forwarding-secret", forwardingSecretString);
mustResave = true;
try (final CommentedFileConfig config = CommentedFileConfig.builder(path)
.defaultData(defaultConfigLocation)
.autosave()
.preserveInsertionOrder()
.sync()
.build()
) {
config.load();

final ConfigurationMigration[] migrations = {
new ForwardingMigration(),
new KeyAuthenticationMigration(),
new MotdMigration()
};

for (final ConfigurationMigration migration : migrations) {
if (migration.shouldMigrate(config)) {
migration.migrate(config, logger);
}
}
} else {
// New handling
forwardingSecretString = System.getenv().getOrDefault("VELOCITY_FORWARDING_SECRET", "");

String forwardingSecretString = System.getenv().getOrDefault(
"VELOCITY_FORWARDING_SECRET", "");
if (forwardingSecretString.isEmpty()) {
String forwardSecretFile = config.get("forwarding-secret-file");
Path secretPath = forwardSecretFile == null
? defaultForwardingSecretPath
: Path.of(forwardSecretFile);
final String forwardSecretFile = config.get("forwarding-secret-file");
final Path secretPath = forwardSecretFile == null
? defaultForwardingSecretPath
: Path.of(forwardSecretFile);
if (Files.exists(secretPath)) {
if (Files.isRegularFile(secretPath)) {
forwardingSecretString = String.join("", Files.readAllLines(secretPath));
} else {
throw new RuntimeException(
"The file " + forwardSecretFile + " is not a valid file or it is a directory.");
"The file " + forwardSecretFile + " is not a valid file or it is a directory.");
}
} else {
throw new RuntimeException("The forwarding-secret-file does not exist.");
}
}
}
forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);

if (configVersion == 1.0 || configVersion == 2.0) {
config.set("force-key-authentication", config.getOrElse("force-key-authentication", true));
config.setComment("force-key-authentication",
"Should the proxy enforce the new public key security standard? By default, this is on.");
config.set("config-version", configVersion == 2.0 ? "2.5" : "1.5");
mustResave = true;
}

String motd = config.getOrElse("motd", "<#09add3>A Velocity Server");

// Old MOTD Migration
if (configVersion < 2.6) {
final String migratedMotd;
// JSON Format Migration
if (motd.strip().startsWith("{")) {
migratedMotd = MiniMessage.miniMessage().serialize(
GsonComponentSerializer.gson().deserialize(motd))
.replace("\\", "");
} else {
// Legacy '&' Format Migration
migratedMotd = MiniMessage.miniMessage().serialize(
LegacyComponentSerializer.legacyAmpersand().deserialize(motd));
final byte[] forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);
final String motd = config.getOrElse("motd", "<#09add3>A Velocity Server");

// Read the rest of the config
final CommentedConfig serversConfig = config.get("servers");
final CommentedConfig forcedHostsConfig = config.get("forced-hosts");
final CommentedConfig advancedConfig = config.get("advanced");
final CommentedConfig queryConfig = config.get("query");
final CommentedConfig metricsConfig = config.get("metrics");
final PlayerInfoForwarding forwardingMode = config.getEnumOrElse(
"player-info-forwarding-mode", PlayerInfoForwarding.NONE);
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
PingPassthroughMode.DISABLED);

final String bind = config.getOrElse("bind", "0.0.0.0:25577");
final int maxPlayers = config.getIntOrElse("show-max-players", 500);
final boolean onlineMode = config.getOrElse("online-mode", true);
final boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true);
final boolean announceForge = config.getOrElse("announce-forge", true);
final boolean preventClientProxyConnections = config.getOrElse(
"prevent-client-proxy-connections", true);
final boolean kickExisting = config.getOrElse("kick-existing-players", false);
final boolean enablePlayerAddressLogging = config.getOrElse(
"enable-player-address-logging", true);

// Throw an exception if the forwarding-secret file is empty and the proxy is using a
// forwarding mode that requires it.
if (forwardingSecret.length == 0
&& (forwardingMode == PlayerInfoForwarding.MODERN
|| forwardingMode == PlayerInfoForwarding.BUNGEEGUARD)) {
throw new RuntimeException("The forwarding-secret file must not be empty.");
}

config.set("motd", migratedMotd);
motd = migratedMotd;

config.setComment("motd",
" What should be the MOTD? This gets displayed when the player adds your server to\n"
+ " their server list. Only MiniMessage format is accepted.");
config.set("config-version", "2.6");
mustResave = true;
}

// Handle any cases where the config needs to be saved again
if (mustResave) {
config.save();
}

// Read the rest of the config
CommentedConfig serversConfig = config.get("servers");
CommentedConfig forcedHostsConfig = config.get("forced-hosts");
CommentedConfig advancedConfig = config.get("advanced");
CommentedConfig queryConfig = config.get("query");
CommentedConfig metricsConfig = config.get("metrics");
PlayerInfoForwarding forwardingMode = config.getEnumOrElse("player-info-forwarding-mode",
PlayerInfoForwarding.NONE);
PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
PingPassthroughMode.DISABLED);

String bind = config.getOrElse("bind", "0.0.0.0:25577");
int maxPlayers = config.getIntOrElse("show-max-players", 500);
Boolean onlineMode = config.getOrElse("online-mode", true);
Boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true);
Boolean announceForge = config.getOrElse("announce-forge", true);
Boolean preventClientProxyConnections = config.getOrElse("prevent-client-proxy-connections",
true);
Boolean kickExisting = config.getOrElse("kick-existing-players", false);
Boolean enablePlayerAddressLogging = config.getOrElse("enable-player-address-logging", true);

// Throw an exception if the forwarding-secret file is empty and the proxy is using a
// forwarding mode that requires it.
if (forwardingSecret.length == 0
&& (forwardingMode == PlayerInfoForwarding.MODERN
|| forwardingMode == PlayerInfoForwarding.BUNGEEGUARD)) {
throw new RuntimeException("The forwarding-secret file must not be empty.");
}

return new VelocityConfiguration(
bind,
motd,
maxPlayers,
onlineMode,
preventClientProxyConnections,
announceForge,
forwardingMode,
forwardingSecret,
kickExisting,
pingPassthroughMode,
enablePlayerAddressLogging,
new Servers(serversConfig),
new ForcedHosts(forcedHostsConfig),
new Advanced(advancedConfig),
new Query(queryConfig),
new Metrics(metricsConfig),
forceKeyAuthentication
);
return new VelocityConfiguration(
bind,
motd,
maxPlayers,
onlineMode,
preventClientProxyConnections,
announceForge,
forwardingMode,
forwardingSecret,
kickExisting,
pingPassthroughMode,
enablePlayerAddressLogging,
new Servers(serversConfig),
new ForcedHosts(forcedHostsConfig),
new Advanced(advancedConfig),
new Query(queryConfig),
new Metrics(metricsConfig),
forceKeyAuthentication
);
}
}

private static String generateRandomString(int length) {
String chars = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890";
StringBuilder builder = new StringBuilder();
Random rnd = new SecureRandom();
/**
* Generates a Random String.
*
* @param length the required string size.
* @return a new random string.
*/
public static String generateRandomString(int length) {
final String chars = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890";
final StringBuilder builder = new StringBuilder();
final Random rnd = new SecureRandom();
for (int i = 0; i < length; i++) {
builder.append(chars.charAt(rnd.nextInt(chars.length())));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.velocitypowered.proxy.config.migration;

import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import java.io.IOException;
import org.apache.logging.log4j.Logger;

/**
* Configuration Migration interface.
*/
public sealed interface ConfigurationMigration
permits ForwardingMigration, KeyAuthenticationMigration, MotdMigration {
boolean shouldMigrate(CommentedFileConfig config);

void migrate(CommentedFileConfig config, Logger logger) throws IOException;

/**
* Gets the configuration version.
*
* @param config the configuration.
* @return configuration version
*/
default double configVersion(CommentedFileConfig config) {
final String stringVersion = config.getOrElse("config-version", "1.0");
try {
return Double.parseDouble(stringVersion);
} catch (Exception e) {
return 1.0;
}
}
}
Loading