/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm; import password.pwm.config.Configuration; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmUnrecoverableException; import password.pwm.http.ContextManager; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.JsonUtil; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogger; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.io.StringWriter; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.TimeUnit; public class PwmEnvironment { private static final PwmLogger LOGGER = PwmLogger.forClass(PwmEnvironment.class); // data elements private final PwmApplicationMode applicationMode; private final Configuration config; private final File applicationPath; private final boolean internalRuntimeInstance; private final File configurationFile; private final ContextManager contextManager; private final Collection<ApplicationFlag> flags; private final Map<ApplicationParameter,String> parameters; private final FileLocker fileLocker; public enum ApplicationParameter { AutoExportHttpsKeyStoreFile, AutoExportHttpsKeyStorePassword, AutoExportHttpsKeyStoreAlias, AutoWriteTomcatConfSourceFile, AutoWriteTomcatConfOutputFile, AppliancePort, ApplianceHostnameFile, ApplianceTokenFile, ; public static ApplicationParameter forString(final String input) { return JavaHelper.readEnumFromString(ApplicationParameter.class, null, input); } } public enum ApplicationFlag { Appliance, Docker, ManageHttps, NoFileLock, CommandLineInstance, ; public static ApplicationFlag forString(final String input) { return JavaHelper.readEnumFromString(ApplicationFlag.class, null, input); } } public enum EnvironmentParameter { applicationPath, applicationFlags, applicationParamFile, ; public String conicalJavaOptionSystemName() { return PwmConstants.PWM_APP_NAME.toLowerCase() + "." + this.toString(); } public String conicalEnvironmentSystemName() { return (PwmConstants.PWM_APP_NAME.toLowerCase() + "_" + this.toString()).toUpperCase(); } public List<String> possibleNames(final String contextName) { final List<String> returnValues = new ArrayList<>(); if (contextName != null) { // java property format <app>.<context>.<paramName> like pwm.pwm.applicationFlag final String value = PwmConstants.PWM_APP_NAME.toLowerCase() + "." + contextName + "." + this.toString(); returnValues.add(value); returnValues.add(value.toUpperCase()); returnValues.add(value.replace(".", "_")); returnValues.add(value.toUpperCase().replace(".", "_")); } { // java property format <app>.<paramName> like pwm.applicationFlag final String value = PwmConstants.PWM_APP_NAME.toLowerCase() + "." + this.toString(); returnValues.add(value); returnValues.add(value.toUpperCase()); returnValues.add(value.replace(".","_")); returnValues.add(value.toUpperCase().replace(".", "_")); } return Collections.unmodifiableList(returnValues); } } private PwmEnvironment( final PwmApplicationMode applicationMode, final Configuration config, final File applicationPath, final boolean internalRuntimeInstance, final File configurationFile, final ContextManager contextManager, final Collection<ApplicationFlag> flags, final Map<ApplicationParameter,String> parameters ) { this.applicationMode = applicationMode == null ? PwmApplicationMode.ERROR : applicationMode; this.config = config; this.applicationPath = applicationPath; this.internalRuntimeInstance = internalRuntimeInstance; this.configurationFile = configurationFile; this.contextManager = contextManager; this.flags = flags == null ? Collections.emptySet() : Collections.unmodifiableSet(new HashSet<>(flags)); this.parameters = parameters == null ? Collections.emptyMap() : Collections.unmodifiableMap(parameters); this.fileLocker = new FileLocker(); verify(); } public PwmApplicationMode getApplicationMode() { return applicationMode; } public Configuration getConfig() { return config; } public File getApplicationPath() { return applicationPath; } public boolean isInternalRuntimeInstance() { return internalRuntimeInstance; } public File getConfigurationFile() { return configurationFile; } public ContextManager getContextManager() { return contextManager; } public Collection<ApplicationFlag> getFlags() { return flags; } public Map<ApplicationParameter, String> getParameters() { return parameters; } private void verify() { } public void verifyIfApplicationPathIsSetProperly() throws PwmUnrecoverableException { final File applicationPath = this.getApplicationPath(); verifyApplicationPath(applicationPath); boolean applicationPathIsWebInfPath = false; if (applicationPath.getAbsolutePath().endsWith("/WEB-INF")) { final File webXmlFile = new File(applicationPath.getAbsolutePath() + File.separator + "web.xml"); if (webXmlFile.exists()) { applicationPathIsWebInfPath = true; } } if (applicationPathIsWebInfPath) { LOGGER.trace("applicationPath appears to be servlet /WEB-INF directory"); } } public PwmEnvironment makeRuntimeInstance( final Configuration configuration ) throws PwmUnrecoverableException { return new Builder(this) .setApplicationMode(PwmApplicationMode.NEW) .setInternalRuntimeInstance(true) .setConfigurationFile(null) .setConfig(configuration) .createPwmEnvironment(); } public static void verifyApplicationPath(final File applicationPath) throws PwmUnrecoverableException { if (applicationPath == null) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, "unable to determine valid applicationPath")); } LOGGER.trace("examining applicationPath of " + applicationPath.getAbsolutePath() + ""); if (!applicationPath.exists()) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, "applicationPath " + applicationPath.getAbsolutePath() + " does not exist")); } if (!applicationPath.canRead()) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, "unable to read from applicationPath " + applicationPath.getAbsolutePath() + "")); } if (!applicationPath.canWrite()) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, "unable to write to applicationPath " + applicationPath.getAbsolutePath() + "")); } final File infoFile = new File(applicationPath.getAbsolutePath() + File.separator + PwmConstants.APPLICATION_PATH_INFO_FILE); LOGGER.trace("checking " + infoFile.getAbsolutePath() + " status"); if (infoFile.exists()) { final String errorMsg = "The file " + infoFile.getAbsolutePath() + " exists, and an applicationPath was not explicitly specified." + " This happens when an applicationPath was previously configured, but is not now being specified." + " An explicit applicationPath parameter must be specified, or the file can be removed if the applicationPath should be changed to the default /WEB-INF directory."; throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, errorMsg)); } } public static class ParseHelper { public static Collection<ApplicationFlag> readApplicationFlagsFromSystem(final String contextName) { final String rawValue = readValueFromSystem(EnvironmentParameter.applicationFlags, contextName); if (rawValue != null) { return parseApplicationFlagValueParameter(rawValue); } return Collections.emptyList(); } public static Map<ApplicationParameter,String> readApplicationParmsFromSystem(final String contextName) { final String rawValue = readValueFromSystem(EnvironmentParameter.applicationParamFile, contextName); if (rawValue != null) { return parseApplicationParamValueParameter(rawValue); } return Collections.emptyMap(); } public static String readValueFromSystem(final PwmEnvironment.EnvironmentParameter parameter, final String contextName) { final List<String> namePossibilities = parameter.possibleNames(contextName); for (final String propertyName : namePossibilities){ final String propValue = System.getProperty(propertyName); if (propValue != null && !propValue.isEmpty()) { return propValue; } } for (final String propertyName : namePossibilities){ final String propValue = System.getenv(propertyName); if (propValue != null && !propValue.isEmpty()) { return propValue; } } return null; } public static Collection<ApplicationFlag> parseApplicationFlagValueParameter(final String input) { if (input == null) { return Collections.emptyList(); } try { final List<String> jsonValues = JsonUtil.deserializeStringList(input); final List<ApplicationFlag> returnFlags = new ArrayList<>(); for (final String value : jsonValues) { final ApplicationFlag flag = ApplicationFlag.forString(value); if (value != null) { returnFlags.add(flag); } else { LOGGER.warn("unknown " + EnvironmentParameter.applicationFlags.toString() + " value: " + input); } } return Collections.unmodifiableList(returnFlags); } catch (Exception e) { // } final List<ApplicationFlag> returnFlags = new ArrayList<>(); for (final String value : input.split(",")) { final ApplicationFlag flag = ApplicationFlag.forString(value); if (value != null) { returnFlags.add(flag); } else { LOGGER.warn("unknown " + EnvironmentParameter.applicationFlags.toString() + " value: " + input); } } return returnFlags; } public static Map<ApplicationParameter,String> parseApplicationParamValueParameter(final String input) { if (input == null) { return Collections.emptyMap(); } Properties propValues = null; try { final Properties newProps = new Properties(); newProps.load(new FileInputStream(new File(input))); propValues = newProps; } catch (Exception e) { LOGGER.warn("error reading properties file '" + input + "' specified by environment setting " + EnvironmentParameter.applicationParamFile.toString() + ", error: " + e.getMessage()); } if (propValues != null) { try { final Map<ApplicationParameter, String> returnParams = new HashMap<>(); for (final Object key : propValues.keySet()) { final ApplicationParameter param = ApplicationParameter.forString(key.toString()); if (param != null) { returnParams.put(param, propValues.getProperty(key.toString())); } else { LOGGER.warn("unknown " + EnvironmentParameter.applicationParamFile.toString() + " value: " + input); } } return Collections.unmodifiableMap(returnParams); } catch (Exception e) { LOGGER.warn("unable to parse jason value of " + EnvironmentParameter.applicationParamFile.toString() + ", error: " + e.getMessage()); } } return Collections.emptyMap(); } } public static class Builder { private PwmApplicationMode applicationMode; private Configuration config; private File applicationPath; private boolean internalRuntimeInstance; private File configurationFile; private ContextManager contextManager; private Collection<ApplicationFlag> flags = new HashSet<>(); private Map<ApplicationParameter,String> params = new HashMap<>(); public Builder(final PwmEnvironment pwmEnvironment) { this.applicationMode = pwmEnvironment.applicationMode; this.config = pwmEnvironment.config; this.applicationPath = pwmEnvironment.applicationPath; this.internalRuntimeInstance = pwmEnvironment.internalRuntimeInstance; this.configurationFile = pwmEnvironment.configurationFile; this.contextManager = pwmEnvironment.contextManager; this.flags = pwmEnvironment.flags; this.params = pwmEnvironment.parameters; } public Builder(final Configuration config, final File applicationPath) { this.config = config; this.applicationPath = applicationPath; } public Builder setApplicationMode(final PwmApplicationMode applicationMode) { if (PwmConstants.TRIAL_MODE && applicationMode == PwmApplicationMode.RUNNING) { LOGGER.info("application is in trial mode"); this.applicationMode = PwmApplicationMode.CONFIGURATION; } else { this.applicationMode = applicationMode; } return this; } public Builder setInternalRuntimeInstance(final boolean internalRuntimeInstance) { this.internalRuntimeInstance = internalRuntimeInstance; return this; } public Builder setConfigurationFile(final File configurationFile) { this.configurationFile = configurationFile; return this; } public Builder setContextManager(final ContextManager contextManager) { this.contextManager = contextManager; return this; } public Builder setFlags(final Collection<ApplicationFlag> flags) { this.flags.clear(); if (flags != null) { this.flags.addAll(flags); } return this; } public Builder setParams(final Map<ApplicationParameter,String> params) { this.params.clear(); if (params != null) { this.params.putAll(params); } return this; } public Builder setConfig(final Configuration config) { this.config = config; return this; } public PwmEnvironment createPwmEnvironment() { return new PwmEnvironment( applicationMode, config, applicationPath, internalRuntimeInstance, configurationFile, contextManager, flags, params ); } } public void attemptFileLock() { fileLocker.attemptFileLock(); } public void releaseFileLock() { fileLocker.releaseFileLock(); } public boolean isFileLocked() { return fileLocker.isLocked(); } public void waitForFileLock() throws PwmUnrecoverableException { final int maxWaitSeconds = this.getFlags().contains(ApplicationFlag.CommandLineInstance) ? 1 : Integer.parseInt(getConfig().readAppProperty(AppProperty.APPLICATION_FILELOCK_WAIT_SECONDS)); final Instant startTime = Instant.now(); final int attemptInterval = 5021; //ms while (!this.isFileLocked() && TimeDuration.fromCurrent(startTime).isShorterThan(maxWaitSeconds, TimeUnit.SECONDS)) { attemptFileLock(); if (!isFileLocked()) { LOGGER.debug("can't establish application file lock after " + TimeDuration.fromCurrent(startTime).asCompactString() + ", will retry;"); JavaHelper.pause(attemptInterval); } } if (!isFileLocked()) { final String errorMsg = "unable to obtain application path file lock"; final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_STARTUP_ERROR,errorMsg); throw new PwmUnrecoverableException(errorInformation); } } private class FileLocker { private FileLock lock; private final File lockfile; FileLocker() { final String lockfileName = config.readAppProperty(AppProperty.APPLICATION_FILELOCK_FILENAME); lockfile = new File(getApplicationPath(), lockfileName); } private boolean lockingAllowed() { return !isInternalRuntimeInstance() && !getFlags().contains(ApplicationFlag.NoFileLock); } public boolean isLocked() { return !lockingAllowed() || lock != null && lock.isValid(); } public void attemptFileLock() { if (lockingAllowed() && !isLocked()) { try { final RandomAccessFile file = new RandomAccessFile(lockfile, "rw"); final FileChannel f = file.getChannel(); lock = f.tryLock(); if (lock != null) { LOGGER.debug("obtained file lock on file " + lockfile.getAbsolutePath() + " lock is valid=" + lock.isValid()); writeLockFileContents(file); } else { LOGGER.debug("unable to obtain file lock on file " + lockfile.getAbsolutePath()); } } catch (Exception e) { LOGGER.error("unable to obtain file lock on file " + lockfile.getAbsolutePath() + " due to error: " + e.getMessage()); } } } void writeLockFileContents(final RandomAccessFile file) { try { final Properties props = new Properties(); props.put("timestamp", JavaHelper.toIsoDate(new Date())); props.put("applicationPath",PwmEnvironment.this.getApplicationPath() == null ? "n/a" : PwmEnvironment.this.getApplicationPath().getAbsolutePath()); props.put("configurationFile", PwmEnvironment.this.getConfigurationFile() == null ? "n/a" : PwmEnvironment.this.getConfigurationFile().getAbsolutePath()); final String comment = PwmConstants.PWM_APP_NAME + " file lock"; final StringWriter stringWriter = new StringWriter(); props.store(stringWriter, comment); file.write(stringWriter.getBuffer().toString().getBytes(PwmConstants.DEFAULT_CHARSET)); } catch (IOException e) { LOGGER.error("unable to write contents of application lock file: " + e.getMessage()); } // do not close FileWriter, otherwise lock is released. } public void releaseFileLock() { if (lock != null && lock.isValid()) { try { lock.release(); } catch (IOException e) { LOGGER.error("error releasing file lock: " + e.getMessage()); } LOGGER.debug("released file lock on file " + lockfile.getAbsolutePath()); } } } }