fix: recover SubstAgent placeholders from the jar, not the live file
The previous approach read the live config.yml back through NIO to
recover the raw ^{ENV_VAR} tokens, on the assumption that SubstAgent
only intercepted java.io.FileInputStream. Current SubstAgent also hooks
sun.nio.fs.UnixNativeDispatcher and sun.nio.ch.FileDispatcherImpl, so
NIO reads come back substituted too and the tokens were unrecoverable.
Reading through a hard link with a non-config extension does not help
either. SubstAgent keys its cache by file descriptor number and only
sanity checks the inode, and a hard link shares the inode of the file it
points at, so a recycled descriptor matches an already consumed entry
and the read reports EOF instead of returning the real bytes.
Take the variable names from the default config bundled in the plugin
jar instead. A jar is not a config path, so that copy keeps its raw
placeholders, verified against the agent. A key is only rewritten when
its in-memory value equals the environment variable the default names
for it, which is the only way SubstAgent could have produced it.
Also use ^{ENV_VAR} for the database credentials, which is the syntax
SubstAgent expands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
10c82714ad
commit
04d31f8b8e
2 files changed
+54
-48
No files matched your search
+52
-46
@@ -1,100 +1,106 @@
|
||||
package com.ghostchu.quickshop.util.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
// Keeps SubstAgent placeholders on disk when the config is written back.
|
||||
// SubstAgent substitutes while the JVM reads through FileInputStream.
|
||||
// Bukkit loads configs that way, so the in-memory value is already resolved.
|
||||
// NIO reads are not intercepted, so the raw tokens are recovered with NIO.
|
||||
// SubstAgent expands ^{ENV_VAR} while the server reads a config file, so the
|
||||
// value held in memory is already resolved and saving it would burn the secret
|
||||
// into the file. The live file cannot be re-read to recover the token, because
|
||||
// SubstAgent intercepts the stream and the NIO read paths alike, so the token
|
||||
// names come from the default config inside the plugin jar instead. A jar is
|
||||
// not a config file, so that copy still carries the raw placeholders.
|
||||
public final class EnvironmentPlaceholders {
|
||||
|
||||
// SubstAgent $ENV_VAR form, plus the older ^{ENV_VAR} form.
|
||||
private static final Pattern PLACEHOLDER = Pattern
|
||||
.compile("\\$[A-Za-z_][A-Za-z0-9_]*|\\^\\{[A-Za-z_][A-Za-z0-9_]*}");
|
||||
// The ^{ENV_VAR} form SubstAgent expands, group 1 is the variable name.
|
||||
private static final Pattern PLACEHOLDER = Pattern.compile("^\\^\\{([A-Za-z_][A-Za-z0-9_]*)}$");
|
||||
|
||||
private EnvironmentPlaceholders() {
|
||||
|
||||
}
|
||||
|
||||
// Reads the placeholder tokens still present in the file on disk.
|
||||
// Maps each config key the bundled default declares to its variable name.
|
||||
@NotNull
|
||||
public static Map<String, String> readTokens(@NotNull File file) {
|
||||
public static Map<String, String> readBundledPlaceholders(@NotNull JavaPlugin plugin) {
|
||||
|
||||
if (!file.isFile()) {
|
||||
InputStream resource = plugin.getResource("config.yml");
|
||||
if (resource == null) {
|
||||
|
||||
return Collections.emptyMap();
|
||||
|
||||
}
|
||||
|
||||
String raw;
|
||||
try {
|
||||
YamlConfiguration bundled;
|
||||
try (InputStreamReader reader = new InputStreamReader(new BufferedInputStream(resource),
|
||||
StandardCharsets.UTF_8))
|
||||
{
|
||||
|
||||
// NIO read, a FileInputStream read would be substituted.
|
||||
raw = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
|
||||
bundled = YamlConfiguration.loadConfiguration(reader);
|
||||
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
|
||||
return Collections.emptyMap();
|
||||
|
||||
}
|
||||
|
||||
YamlConfiguration onDisk = new YamlConfiguration();
|
||||
try {
|
||||
Map<String, String> placeholders = new LinkedHashMap<>();
|
||||
for (String key : bundled.getKeys(true)) {
|
||||
|
||||
onDisk.loadFromString(raw);
|
||||
Object value = bundled.get(key);
|
||||
if (!(value instanceof String string)) {
|
||||
|
||||
} catch (InvalidConfigurationException e) {
|
||||
continue;
|
||||
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
}
|
||||
Matcher matcher = PLACEHOLDER.matcher(string.trim());
|
||||
if (matcher.matches()) {
|
||||
|
||||
Map<String, String> tokens = new LinkedHashMap<>();
|
||||
for (String key : onDisk.getKeys(true)) {
|
||||
|
||||
Object value = onDisk.get(key);
|
||||
if (value instanceof String string && PLACEHOLDER.matcher(string).find()) {
|
||||
|
||||
tokens.put(key, string);
|
||||
placeholders.put(key, matcher.group(1));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return tokens;
|
||||
return placeholders;
|
||||
|
||||
}
|
||||
|
||||
// Saves the config without persisting substituted placeholder values.
|
||||
public static void saveConfig(@NotNull JavaPlugin plugin) {
|
||||
|
||||
File file = new File(plugin.getDataFolder(), "config.yml");
|
||||
Map<String, String> tokens = readTokens(file);
|
||||
if (tokens.isEmpty()) {
|
||||
|
||||
plugin.saveConfig();
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
FileConfiguration config = plugin.getConfig();
|
||||
Map<String, Object> substituted = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> token : tokens.entrySet()) {
|
||||
Map<String, Object> resolved = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> placeholder : readBundledPlaceholders(plugin).entrySet()) {
|
||||
|
||||
substituted.put(token.getKey(), config.get(token.getKey()));
|
||||
config.set(token.getKey(), token.getValue());
|
||||
String variable = System.getenv(placeholder.getValue());
|
||||
if (variable == null) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
Object current = config.get(placeholder.getKey());
|
||||
// Only a value equal to the variable can have come from SubstAgent.
|
||||
if (!(current instanceof String string) || !string.equals(variable)) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
resolved.put(placeholder.getKey(), current);
|
||||
config.set(placeholder.getKey(), "^{" + placeholder.getValue() + "}");
|
||||
|
||||
}
|
||||
|
||||
@@ -105,7 +111,7 @@ public final class EnvironmentPlaceholders {
|
||||
} finally {
|
||||
|
||||
// Restore the resolved values for the running server.
|
||||
for (Map.Entry<String, Object> value : substituted.entrySet()) {
|
||||
for (Map.Entry<String, Object> value : resolved.entrySet()) {
|
||||
|
||||
config.set(value.getKey(), value.getValue());
|
||||
|
||||
|
||||
@@ -132,9 +132,9 @@ database:
|
||||
# The database names. (Only required if mysql is true)
|
||||
database: quickshop
|
||||
# The database username. (Only required if mysql is true)
|
||||
user: $MARIADB_USER
|
||||
user: ^{MARIADB_USER}
|
||||
# The database password. (Only required if mysql is true)
|
||||
password: $MARIADB_PASSWORD
|
||||
password: ^{MARIADB_PASSWORD}
|
||||
# Set prefix to "none" to remove prefix (Both local and remote will be used).
|
||||
prefix: none
|
||||
# Should QuickShop use SSL for database connections? (Only required if mysql is true)
|
||||
|
||||
Reference in new issue
Block a user