Skip to content
Open
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
4 changes: 4 additions & 0 deletions server/conf/mirth.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Mirth Connect configuration file
#
# Put local customizations in files under conf/mirth.properties.d instead of
# editing this file, so a new release's mirth.properties can be dropped in
# without hand-merging. See conf/mirth.properties.d/mirth.properties.example.

# directories
dir.appdata = appdata
Expand Down
21 changes: 21 additions & 0 deletions server/conf/mirth.properties.d/mirth.properties.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copy this file to a name ending in .properties (for example 10-local.properties)
# to activate it. Files in this directory named *.properties are loaded on top of
# conf/mirth.properties at startup, in lexical filename order: later files
# override earlier ones, and any file here overrides mirth.properties itself.
#
# Keeping customizations here instead of in mirth.properties means an upgrade
# can replace mirth.properties without hand-merging your changes back in.
#
# Exception: database.password and database-readonly.password are encrypted in
# place in mirth.properties; a value here stays plaintext. keystore.storepass
# and keystore.keypass are auto-generated on first boot only when BOTH are
# still the shipped defaults - overriding just one here leaves the other at
# its published default. Leave these four in mirth.properties.
#
# Files are parsed with java.util.Properties semantics. If a key appears more
# than once, the last occurrence wins; there are no multi-valued properties. For
# list properties such as https.client.protocols, use a single comma-separated
# value.

# http.port = 8081
# https.port = 8444
24 changes: 14 additions & 10 deletions server/src/main/java/com/mirth/connect/server/Mirth.java
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,16 @@ public void run() {
* @return true if the resources required by the server have been successfully loaded
*/
public boolean initResources() {
InputStream mirthPropertiesStream = null;

try {
mirthPropertiesStream = ResourceUtil.getResourceStream(this.getClass(), "mirth.properties");
mirthProperties = PropertiesConfigurationUtil.create(mirthPropertiesStream);
} catch (Exception e) {
logger.error("could not load mirth.properties", e);
} finally {
IOUtils.closeQuietly(mirthPropertiesStream);
// Read the shared, drop-in-merged copy the configuration controller already loaded, rather
// than reloading and re-merging mirth.properties here.
mirthProperties = configurationController.getPropertiesConfiguration();

// Refuse to start if configuration could not be loaded cleanly (e.g. an unreadable
// mirth.properties.d drop-in file) rather than run with a silently reverted configuration.
String configurationLoadError = configurationController.getConfigurationLoadError();
if (configurationLoadError != null) {
logger.error("Refusing to start: " + configurationLoadError);
return false;
}

InputStream versionPropertiesStream = null;
Expand Down Expand Up @@ -221,7 +222,10 @@ public void startup() {
configurationController.initializeSecuritySettings();
configurationController.initializeDatabaseSettings();

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

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

package com.mirth.connect.server.controllers;

import java.io.InputStream;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
Expand All @@ -21,6 +22,7 @@
import com.mirth.commons.encryption.Digester;
import com.mirth.commons.encryption.Encryptor;
import com.mirth.connect.client.core.ControllerException;
import com.mirth.connect.client.core.PropertiesConfigurationUtil;
import com.mirth.connect.model.ChannelDependency;
import com.mirth.connect.model.ChannelMetadata;
import com.mirth.connect.model.ChannelTag;
Expand All @@ -32,6 +34,8 @@
import com.mirth.connect.model.ServerConfiguration;
import com.mirth.connect.model.ServerSettings;
import com.mirth.connect.model.UpdateSettings;
import com.mirth.connect.server.extprops.DropInProperties;
import com.mirth.connect.server.util.ResourceUtil;
import com.mirth.connect.util.ConfigurationProperty;
import com.mirth.connect.util.ConnectionTestResponse;

Expand Down Expand Up @@ -79,6 +83,32 @@ public static ConfigurationController getInstance() {
*/
public abstract void updatePropertiesConfiguration(PropertiesConfiguration config);

/**
* Returns the loaded mirth.properties configuration, with any conf/mirth.properties.d drop-in
* overrides applied. Server-side consumers should read from this shared copy rather than loading
* and merging the file themselves. The returned configuration is owned by the controller;
* consumers must treat it as read-only, as the controller writes to it during startup (for
* example, when it generates keystore passwords). This default loads on demand;
* DefaultConfigurationController overrides it to return the copy it already holds.
*/
public PropertiesConfiguration getPropertiesConfiguration() {
try (InputStream is = ResourceUtil.getResourceStream(getClass(), "mirth.properties")) {
return DropInProperties.overlay(PropertiesConfigurationUtil.create(is), ResourceUtil.getMirthPropertiesDropInDirectory());
} catch (Exception e) {
return PropertiesConfigurationUtil.create();
}
}

/**
* Returns a message describing a fatal configuration problem that must prevent the server from
* starting, such as an unreadable conf/mirth.properties.d drop-in file, or null when
* configuration loaded cleanly. The default reports no error; DefaultConfigurationController
* overrides it.
*/
public String getConfigurationLoadError() {
return null;
}

/**
* Returns the default encryptor.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
import com.mirth.connect.model.converters.ObjectXMLSerializer;
import com.mirth.connect.plugins.directoryresource.DirectoryResourceProperties;
import com.mirth.connect.server.ExtensionLoader;
import com.mirth.connect.server.extprops.DropInProperties;
import com.mirth.connect.server.mybatis.KeyValuePair;
import com.mirth.connect.server.tools.ClassPathResource;
import com.mirth.connect.server.util.DatabaseUtil;
Expand Down Expand Up @@ -166,6 +167,12 @@ public class DefaultConfigurationController extends ConfigurationController {
private static PropertiesConfiguration versionConfig = PropertiesConfigurationUtil.create();
private static FileBasedConfigurationBuilder<PropertiesConfiguration> mirthConfigBuilder = PropertiesConfigurationUtil.createBuilder();
protected static PropertiesConfiguration mirthConfig = PropertiesConfigurationUtil.create();
// The file-backed configuration that saveMirthConfig() writes. Kept separate from mirthConfig
// so that values from mirth.properties.d drop-in files are never baked into mirth.properties.
protected static PropertiesConfiguration mirthFileConfig = mirthConfig;
// Set when a mirth.properties.d drop-in file cannot be read, so the server refuses to start
// rather than run with a silently reverted configuration. Null when configuration loaded cleanly.
private static volatile String configurationLoadError;
private static EncryptionSettings encryptionConfig;
private static DatabaseSettings databaseConfig;
private static String apiBypassword;
Expand Down Expand Up @@ -229,15 +236,28 @@ public void initialize() {
try {
// Delimiter parsing disabled by default so getString() returns the whole property, even if there are commas
mirthConfigBuilder = PropertiesConfigurationUtil.createBuilder(new File(ClassPathResource.getResourceURI("mirth.properties")));
mirthConfig = mirthConfigBuilder.getConfiguration();
mirthFileConfig = mirthConfigBuilder.getConfiguration();
// Also assign the read view now so anything reading during migration sees real values
mirthConfig = mirthFileConfig;

MigrationController.getInstance().migrateConfiguration(mirthConfig);
MigrationController.getInstance().migrateConfiguration(mirthFileConfig);
try {
mirthConfigBuilder.save();
} catch (ConfigurationException e) {
logger.error("Unable to update mirth.properties version during migration.", e);
}

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

// load the server version
versionPropertiesStream = ResourceUtil.getResourceStream(this.getClass(), "version.properties");
versionConfig = PropertiesConfigurationUtil.create(versionPropertiesStream);
Expand Down Expand Up @@ -1245,11 +1265,11 @@ public void initializeSecuritySettings() {
*/
if (Arrays.equals(keyStorePassword, DEFAULT_STOREPASS.toCharArray()) && Arrays.equals(keyPassword, DEFAULT_STOREPASS.toCharArray())) {
String keyStorePasswordStr = generateNewPassword();
mirthConfig.setProperty("keystore.storepass", keyStorePasswordStr);
setMirthConfigProperty("keystore.storepass", keyStorePasswordStr);
keyStorePassword = keyStorePasswordStr.toCharArray();

String keyPasswordStr = generateNewPassword();
mirthConfig.setProperty("keystore.keypass", keyPasswordStr);
setMirthConfigProperty("keystore.keypass", keyPasswordStr);
keyPassword = keyPasswordStr.toCharArray();

saveMirthConfig();
Expand Down Expand Up @@ -1329,18 +1349,26 @@ private void updateDatabasePassword(Encryptor encryptor, String password, boolea

if (!StringUtils.startsWith(encryptedPassword, "{")) {
// Re-encrypt if still using the old-style format
mirthConfig.setProperty(readOnly ? DatabaseConstants.DATABASE_READONLY_PASSWORD : DatabaseConstants.DATABASE_PASSWORD, EncryptionSettings.ENCRYPTION_PREFIX + encryptor.encrypt(decryptedPassword));
setMirthConfigProperty(readOnly ? DatabaseConstants.DATABASE_READONLY_PASSWORD : DatabaseConstants.DATABASE_PASSWORD, EncryptionSettings.ENCRYPTION_PREFIX + encryptor.encrypt(decryptedPassword));
saveMirthConfig();
}
} else if (StringUtils.isNotBlank(password)) {
// encrypt the password and write it back to the file
String encryptedPassword = EncryptionSettings.ENCRYPTION_PREFIX + encryptor.encrypt(password);
mirthConfig.setProperty(readOnly ? DatabaseConstants.DATABASE_READONLY_PASSWORD : DatabaseConstants.DATABASE_PASSWORD, encryptedPassword);
setMirthConfigProperty(readOnly ? DatabaseConstants.DATABASE_READONLY_PASSWORD : DatabaseConstants.DATABASE_PASSWORD, encryptedPassword);

saveMirthConfig();
}
}

private void setMirthConfigProperty(String key, Object value) {
mirthConfig.setProperty(key, value);

if (mirthFileConfig != mirthConfig) {
mirthFileConfig.setProperty(key, value);
}
}

private void saveMirthConfig() throws FileNotFoundException, ConfigurationException {
/*
* Save using a FileOutputStream so that the file will be saved to the proper location, even
Expand All @@ -1350,7 +1378,7 @@ private void saveMirthConfig() throws FileNotFoundException, ConfigurationExcept
OutputStream os = new FileOutputStream(new File(confDir, "mirth.properties"));

try {
PropertiesConfigurationUtil.saveTo(mirthConfig, os);
PropertiesConfigurationUtil.saveTo(mirthFileConfig, os);
} finally {
ResourceUtil.closeResourceQuietly(os);
}
Expand Down Expand Up @@ -1438,6 +1466,16 @@ public void migrateKeystore() {
}
}

@Override
public PropertiesConfiguration getPropertiesConfiguration() {
return mirthConfig;
}

@Override
public String getConfigurationLoadError() {
return configurationLoadError;
}

@Override
public void updatePropertiesConfiguration(PropertiesConfiguration config) {
config.copy(mirthConfig);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright (c) Mirth Corporation. All rights reserved.
*
* http://www.mirthcorp.com
*
* The software in this package is published under the terms of the MPL license a copy of which has
* been included with this distribution in the LICENSE.txt file.
*/

package com.mirth.connect.server.extprops;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Properties;

import org.apache.commons.configuration2.ConfigurationUtils;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;

/**
* Applies drop-in override files from a mirth.properties.d directory on top of an already-loaded
* configuration. Files directly under the directory whose names end in ".properties" are applied
* in lexical filename order: later files override earlier ones, and all of them override the base
* configuration. Merge semantics come from java.util.Properties.load(), where the last occurrence
* of a key wins.
*
* This class lives in the extprops package because it is shared by the launcher jar and the server
* jar, and must not reference classes outside this package and the launcher's classpath.
*/
public class DropInProperties {

/**
* Loads drop-in files into the given properties, overriding any existing values. Files that
* cannot be read are logged and skipped.
*/
public static void load(Properties properties, File dropInDir, LoggerWrapper logger) {
File[] files = listDropInFiles(dropInDir);

if (files == null) {
if (isUnlistableDirectory(dropInDir)) {
logger.error("Unable to list drop-in properties directory: " + dropInDir.getPath());
}
return;
}

for (File file : files) {
try (InputStream is = new FileInputStream(file)) {
properties.load(is);
} catch (IOException | IllegalArgumentException e) {
// IllegalArgumentException covers a malformed \\uXXXX escape in Properties.load()
logger.error("Unable to read drop-in properties file: " + file.getPath(), e);
}
}
}

/**
* Returns a configuration with any drop-in files applied on top of the given base
* configuration. The base configuration is never modified; it is returned as-is when there are
* no drop-in files. A drop-in file that cannot be read or parsed throws a ConfigurationException
* so that the caller can fail closed rather than start with a partially applied configuration.
*/
public static PropertiesConfiguration overlay(PropertiesConfiguration base, File dropInDir) throws ConfigurationException {
File[] files = listDropInFiles(dropInDir);

if (files == null) {
if (isUnlistableDirectory(dropInDir)) {
throw new ConfigurationException("Unable to list drop-in properties directory: " + dropInDir.getPath());
}
return base;
}

if (files.length == 0) {
return base;
}

Properties dropIns = new Properties();

for (File file : files) {
try (InputStream is = new FileInputStream(file)) {
dropIns.load(is);
} catch (IOException | IllegalArgumentException e) {
// IllegalArgumentException covers a malformed \\uXXXX escape in Properties.load().
// A file that cannot be read or parsed is fatal here so a broken drop-in fails startup
// rather than silently reverting to the base configuration.
throw new ConfigurationException("Unable to read drop-in properties file: " + file.getPath(), e);
}
}

PropertiesConfiguration merged = new PropertiesConfiguration();
ConfigurationUtils.copy(base, merged);

for (String key : dropIns.stringPropertyNames()) {
merged.setProperty(key, dropIns.getProperty(key));
}

return merged;
}

private static File[] listDropInFiles(File dropInDir) {
File[] files = dropInDir == null ? null : dropInDir.listFiles(file -> file.isFile() && file.getName().endsWith(".properties"));

if (files != null) {
Arrays.sort(files);
}

return files;
}

/** listFiles() returns null both for a missing directory and an unlistable one; only the latter deserves noise. */
private static boolean isUnlistableDirectory(File dropInDir) {
return dropInDir != null && dropInDir.isDirectory();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ private ExtensionStatuses() {
} finally {
IOUtils.closeQuietly(is);
}
DropInProperties.load(mirthProperties, new File("./conf/mirth.properties.d"), logger);
} catch (Exception e) {
logger.error("Unable to read mirth.properties.", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.mirth.connect.server.extprops.DropInProperties;
import com.mirth.connect.server.extprops.ExtensionStatuses;
import com.mirth.connect.server.extprops.LoggerWrapper;

Expand Down Expand Up @@ -77,6 +78,7 @@ public static void main(String[] args) {

try (FileInputStream inputStream = new FileInputStream(new File(MIRTH_PROPERTIES_FILE))) {
mirthProperties.load(inputStream);
DropInProperties.load(mirthProperties, new File(MIRTH_PROPERTIES_FILE + ".d"), logger);
includeCustomLib = mirthProperties.getProperty(PROPERTY_INCLUDE_CUSTOM_LIB);
createAppdataDir(mirthProperties);
} catch (Exception e) {
Expand Down
Loading
Loading