Skip to content

Commit 5d0c677

Browse files
committed
Share one merged configuration and fail closed on a bad drop-in file
Add ConfigurationController.getPropertiesConfiguration(), which returns mirth.properties with the conf/mirth.properties.d drop-ins already applied. Mirth and WebStartServlet now read that single shared copy instead of each loading and merging the file themselves. The returned configuration is owned by the controller and must be treated as read-only; the controller writes to it during startup (for example when it generates keystore passwords), and consumers only read it. A drop-in file that cannot be read or parsed now fails closed. Previously overlay() threw during controller initialization, where the exception was swallowed and the server came up on the base configuration with the drop-ins silently dropped; if a drop-in was hardening a setting, that reverts to the weaker base value. Catch the failure, record it, and refuse to start with a clear message. The launcher's early readers stay lenient and skip a bad file with a warning, matching how they already treat mirth.properties. A malformed \uXXXX escape is treated the same as an unreadable file. Add a regression test that a write to the merged configuration does not leak back into the base file saved to mirth.properties. Signed-off-by: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com>
1 parent 1bf2c47 commit 5d0c677

6 files changed

Lines changed: 98 additions & 31 deletions

File tree

‎server/src/main/java/com/mirth/connect/server/Mirth.java‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
import com.mirth.connect.server.controllers.MigrationController;
6060
import com.mirth.connect.server.controllers.ScriptController;
6161
import com.mirth.connect.server.controllers.UsageController;
62-
import com.mirth.connect.server.extprops.DropInProperties;
6362
import com.mirth.connect.server.controllers.UserController;
6463
import com.mirth.connect.server.logging.JuliToLog4JService;
6564
import com.mirth.connect.server.logging.LogOutputStream;
@@ -183,18 +182,16 @@ public void run() {
183182
* @return true if the resources required by the server have been successfully loaded
184183
*/
185184
public boolean initResources() {
186-
InputStream mirthPropertiesStream = null;
187-
188-
try {
189-
mirthPropertiesStream = ResourceUtil.getResourceStream(this.getClass(), "mirth.properties");
190-
mirthProperties = PropertiesConfigurationUtil.create(mirthPropertiesStream);
191-
mirthProperties = DropInProperties.overlay(mirthProperties, ResourceUtil.getMirthPropertiesDropInDirectory());
192-
} catch (Exception e) {
193-
// Reset so a failed drop-in overlay aborts startup instead of booting with a partial configuration
194-
mirthProperties = PropertiesConfigurationUtil.create();
195-
logger.error("could not load mirth.properties", e);
196-
} finally {
197-
IOUtils.closeQuietly(mirthPropertiesStream);
185+
// Read the shared, drop-in-merged copy the configuration controller already loaded, rather
186+
// than reloading and re-merging mirth.properties here.
187+
mirthProperties = configurationController.getPropertiesConfiguration();
188+
189+
// Refuse to start if configuration could not be loaded cleanly (e.g. an unreadable
190+
// mirth.properties.d drop-in file) rather than run with a silently reverted configuration.
191+
String configurationLoadError = configurationController.getConfigurationLoadError();
192+
if (configurationLoadError != null) {
193+
logger.error("Refusing to start: " + configurationLoadError);
194+
return false;
198195
}
199196

200197
InputStream versionPropertiesStream = null;
@@ -225,7 +222,10 @@ public void startup() {
225222
configurationController.initializeSecuritySettings();
226223
configurationController.initializeDatabaseSettings();
227224

228-
// Refresh the in-memory config in case the configuration controller changed it
225+
// Pull in any settings the controller changed during initialization (e.g. generated keystore
226+
// passwords). With the default controller mirthProperties is already the controller's own
227+
// configuration, so this is a no-op; a controller that hands out a separate configuration is
228+
// refreshed here.
229229
configurationController.updatePropertiesConfiguration(mirthProperties);
230230

231231
try {

‎server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package com.mirth.connect.server.controllers;
1111

12+
import java.io.InputStream;
1213
import java.util.Calendar;
1314
import java.util.List;
1415
import java.util.Locale;
@@ -21,6 +22,7 @@
2122
import com.mirth.commons.encryption.Digester;
2223
import com.mirth.commons.encryption.Encryptor;
2324
import com.mirth.connect.client.core.ControllerException;
25+
import com.mirth.connect.client.core.PropertiesConfigurationUtil;
2426
import com.mirth.connect.model.ChannelDependency;
2527
import com.mirth.connect.model.ChannelMetadata;
2628
import com.mirth.connect.model.ChannelTag;
@@ -32,6 +34,8 @@
3234
import com.mirth.connect.model.ServerConfiguration;
3335
import com.mirth.connect.model.ServerSettings;
3436
import com.mirth.connect.model.UpdateSettings;
37+
import com.mirth.connect.server.extprops.DropInProperties;
38+
import com.mirth.connect.server.util.ResourceUtil;
3539
import com.mirth.connect.util.ConfigurationProperty;
3640
import com.mirth.connect.util.ConnectionTestResponse;
3741

@@ -79,6 +83,32 @@ public static ConfigurationController getInstance() {
7983
*/
8084
public abstract void updatePropertiesConfiguration(PropertiesConfiguration config);
8185

86+
/**
87+
* Returns the loaded mirth.properties configuration, with any conf/mirth.properties.d drop-in
88+
* overrides applied. Server-side consumers should read from this shared copy rather than loading
89+
* and merging the file themselves. The returned configuration is owned by the controller;
90+
* consumers must treat it as read-only, as the controller writes to it during startup (for
91+
* example, when it generates keystore passwords). This default loads on demand;
92+
* DefaultConfigurationController overrides it to return the copy it already holds.
93+
*/
94+
public PropertiesConfiguration getPropertiesConfiguration() {
95+
try (InputStream is = ResourceUtil.getResourceStream(getClass(), "mirth.properties")) {
96+
return DropInProperties.overlay(PropertiesConfigurationUtil.create(is), ResourceUtil.getMirthPropertiesDropInDirectory());
97+
} catch (Exception e) {
98+
return PropertiesConfigurationUtil.create();
99+
}
100+
}
101+
102+
/**
103+
* Returns a message describing a fatal configuration problem that must prevent the server from
104+
* starting, such as an unreadable conf/mirth.properties.d drop-in file, or null when
105+
* configuration loaded cleanly. The default reports no error; DefaultConfigurationController
106+
* overrides it.
107+
*/
108+
public String getConfigurationLoadError() {
109+
return null;
110+
}
111+
82112
/**
83113
* Returns the default encryptor.
84114
*

‎server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@
118118
import com.mirth.connect.model.converters.ObjectXMLSerializer;
119119
import com.mirth.connect.plugins.directoryresource.DirectoryResourceProperties;
120120
import com.mirth.connect.server.ExtensionLoader;
121-
import com.mirth.connect.server.mybatis.KeyValuePair;
122121
import com.mirth.connect.server.extprops.DropInProperties;
122+
import com.mirth.connect.server.mybatis.KeyValuePair;
123123
import com.mirth.connect.server.tools.ClassPathResource;
124124
import com.mirth.connect.server.util.DatabaseUtil;
125125
import com.mirth.connect.server.util.PasswordRequirementsChecker;
@@ -170,6 +170,9 @@ public class DefaultConfigurationController extends ConfigurationController {
170170
// The file-backed configuration that saveMirthConfig() writes. Kept separate from mirthConfig
171171
// so that values from mirth.properties.d drop-in files are never baked into mirth.properties.
172172
protected static PropertiesConfiguration mirthFileConfig = mirthConfig;
173+
// Set when a mirth.properties.d drop-in file cannot be read, so the server refuses to start
174+
// rather than run with a silently reverted configuration. Null when configuration loaded cleanly.
175+
private static volatile String configurationLoadError;
173176
private static EncryptionSettings encryptionConfig;
174177
private static DatabaseSettings databaseConfig;
175178
private static String apiBypassword;
@@ -244,7 +247,16 @@ public void initialize() {
244247
logger.error("Unable to update mirth.properties version during migration.", e);
245248
}
246249

247-
mirthConfig = DropInProperties.overlay(mirthFileConfig, ResourceUtil.getMirthPropertiesDropInDirectory());
250+
try {
251+
mirthConfig = DropInProperties.overlay(mirthFileConfig, ResourceUtil.getMirthPropertiesDropInDirectory());
252+
} catch (ConfigurationException e) {
253+
// Fail closed: a broken drop-in file must stop startup rather than silently reverting
254+
// to the base configuration, which could serve weaker settings than the administrator
255+
// intended. mirthConfig stays at the base config and getConfigurationLoadError() reports
256+
// the problem so the server refuses to start.
257+
configurationLoadError = "Unable to load conf/mirth.properties.d drop-in configuration: " + e.getMessage();
258+
logger.error(configurationLoadError, e);
259+
}
248260

249261
// load the server version
250262
versionPropertiesStream = ResourceUtil.getResourceStream(this.getClass(), "version.properties");
@@ -1454,6 +1466,16 @@ public void migrateKeystore() {
14541466
}
14551467
}
14561468

1469+
@Override
1470+
public PropertiesConfiguration getPropertiesConfiguration() {
1471+
return mirthConfig;
1472+
}
1473+
1474+
@Override
1475+
public String getConfigurationLoadError() {
1476+
return configurationLoadError;
1477+
}
1478+
14571479
@Override
14581480
public void updatePropertiesConfiguration(PropertiesConfiguration config) {
14591481
config.copy(mirthConfig);

‎server/src/main/java/com/mirth/connect/server/extprops/DropInProperties.java‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public static void load(Properties properties, File dropInDir, LoggerWrapper log
4949
for (File file : files) {
5050
try (InputStream is = new FileInputStream(file)) {
5151
properties.load(is);
52-
} catch (IOException e) {
52+
} catch (IOException | IllegalArgumentException e) {
53+
// IllegalArgumentException covers a malformed \\uXXXX escape in Properties.load()
5354
logger.error("Unable to read drop-in properties file: " + file.getPath(), e);
5455
}
5556
}
@@ -58,7 +59,8 @@ public static void load(Properties properties, File dropInDir, LoggerWrapper log
5859
/**
5960
* Returns a configuration with any drop-in files applied on top of the given base
6061
* configuration. The base configuration is never modified; it is returned as-is when there are
61-
* no drop-in files.
62+
* no drop-in files. A drop-in file that cannot be read or parsed throws a ConfigurationException
63+
* so that the caller can fail closed rather than start with a partially applied configuration.
6264
*/
6365
public static PropertiesConfiguration overlay(PropertiesConfiguration base, File dropInDir) throws ConfigurationException {
6466
File[] files = listDropInFiles(dropInDir);
@@ -79,7 +81,10 @@ public static PropertiesConfiguration overlay(PropertiesConfiguration base, File
7981
for (File file : files) {
8082
try (InputStream is = new FileInputStream(file)) {
8183
dropIns.load(is);
82-
} catch (IOException e) {
84+
} catch (IOException | IllegalArgumentException e) {
85+
// IllegalArgumentException covers a malformed \\uXXXX escape in Properties.load().
86+
// A file that cannot be read or parsed is fatal here so a broken drop-in fails startup
87+
// rather than silently reverting to the base configuration.
8388
throw new ConfigurationException("Unable to read drop-in properties file: " + file.getPath(), e);
8489
}
8590
}

‎server/src/main/java/com/mirth/connect/server/servlets/WebStartServlet.java‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
import com.mirth.connect.server.controllers.ConfigurationController;
5050
import com.mirth.connect.server.controllers.ControllerFactory;
5151
import com.mirth.connect.server.controllers.ExtensionController;
52-
import com.mirth.connect.server.extprops.DropInProperties;
5352
import com.mirth.connect.server.tools.ClassPathResource;
5453
import com.mirth.connect.server.util.ResourceUtil;
5554
import com.mirth.connect.util.MirthSSLUtil;
@@ -360,17 +359,8 @@ private String getDigest(File directory, String filePath) throws Exception {
360359
}
361360

362361
protected PropertiesConfiguration getMirthProperties() throws FileNotFoundException, ConfigurationException {
363-
PropertiesConfiguration mirthProperties = PropertiesConfigurationUtil.create();
364-
365-
InputStream mirthPropsIs = null;
366-
try {
367-
mirthPropsIs = ResourceUtil.getResourceStream(getClass(), "mirth.properties");
368-
mirthProperties = PropertiesConfigurationUtil.create(mirthPropsIs);
369-
mirthProperties = DropInProperties.overlay(mirthProperties, ResourceUtil.getMirthPropertiesDropInDirectory());
370-
} finally {
371-
ResourceUtil.closeResourceQuietly(mirthPropsIs);
372-
}
373-
return mirthProperties;
362+
// Read the shared, drop-in-merged copy rather than reloading the file on every request.
363+
return configurationController.getPropertiesConfiguration();
374364
}
375365

376366
private String getContextPathProp(PropertiesConfiguration mirthProperties) {

‎server/src/test/java/com/mirth/connect/server/extprops/DropInPropertiesTest.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,26 @@ public void overlayOverridesAndAddsWithoutModifyingBase() throws Exception {
6060
assertFalse(base.containsKey("c"));
6161
}
6262

63+
@Test
64+
public void overlayResultIsIndependentOfBase() throws Exception {
65+
PropertiesConfiguration base = new PropertiesConfiguration();
66+
base.setProperty("a", "base");
67+
68+
File dir = tempFolder.newFolder();
69+
writeFile(dir, "10-first.properties", "b = dropin\n");
70+
71+
PropertiesConfiguration merged = DropInProperties.overlay(base, dir);
72+
73+
// Writing to the merged result (as the controller does when it re-encrypts a password or
74+
// generates a keystore password) must not leak back into the base configuration that
75+
// saveMirthConfig() writes out to mirth.properties.
76+
merged.setProperty("a", "changed");
77+
merged.setProperty("c", "new");
78+
79+
assertEquals("base", base.getString("a"));
80+
assertFalse(base.containsKey("c"));
81+
}
82+
6383
@Test
6484
public void overlayKeepsCommaValuesWhole() throws Exception {
6585
PropertiesConfiguration base = new PropertiesConfiguration();

0 commit comments

Comments
 (0)