diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/environment/Environment.java b/guacamole-ext/src/main/java/org/apache/guacamole/environment/Environment.java index 43f8f7560..16f8f58d3 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/environment/Environment.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/environment/Environment.java @@ -19,9 +19,11 @@ package org.apache.guacamole.environment; +import org.apache.guacamole.properties.GuacamoleProperties; import java.io.File; import java.util.Map; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleUnsupportedException; import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration; import org.apache.guacamole.properties.BooleanGuacamoleProperty; import org.apache.guacamole.properties.GuacamoleProperty; @@ -162,4 +164,24 @@ public interface Environment { public GuacamoleProxyConfiguration getDefaultGuacamoleProxyConfiguration() throws GuacamoleException; + /** + * Adds another possible source of Guacamole configuration properties to + * this Environment. Properties not already defined by other sources of + * Guacamole configuration properties will alternatively be read from the + * given {@link GuacamoleProperties}. + * + * @param properties + * The GuacamoleProperties to add to this Environment. + * + * @throws GuacamoleException + * If the given GuacamoleProperties cannot be added, or if this + * Environment does not support this operation. + */ + public default void addGuacamoleProperties(GuacamoleProperties properties) + throws GuacamoleException { + throw new GuacamoleUnsupportedException(String.format("%s does not " + + "support dynamic definition of Guacamole properties.", + getClass())); + } + } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/properties/FileGuacamoleProperties.java b/guacamole-ext/src/main/java/org/apache/guacamole/properties/FileGuacamoleProperties.java new file mode 100644 index 000000000..8be0b4e25 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/properties/FileGuacamoleProperties.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.properties; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; + +/** + * GuacamoleProperties implementation which reads all properties from a + * standard Java properties file. + */ +public class FileGuacamoleProperties extends PropertiesGuacamoleProperties { + + /** + * Reads the given Java properties file, storing all property name/value + * pairs in a new {@link Properties} object. + * + * @param propertiesFile + * The Java properties file to read. + * + * @return + * A new Properties containing all property name/value pairs defined in + * the given file. + * + * @throws GuacamoleException + * If an error prevents reading the given Java properties file. + */ + private static Properties read(File propertiesFile) throws GuacamoleException { + + // Fail early if file simply does not exist + if (!propertiesFile.exists()) + throw new GuacamoleServerException(String.format("\"%s\" does not " + + "exist.", propertiesFile)); + + // Load properties from stream, if any, always closing stream when done + Properties properties = new Properties(); + try (InputStream stream = new FileInputStream(propertiesFile)) { + properties.load(stream); + } + catch (IOException e) { + throw new GuacamoleServerException(String.format("\"%s\" cannot " + + "be read: %s", propertiesFile, e.getMessage()), e); + } + + return properties; + + } + + /** + * Creates a new FileGuacamoleProperties which reads all properties from + * the given standard Java properties file. + * + * @param propertiesFile + * The Java properties file to read. + * + * @throws GuacamoleException + * If an error prevents reading the given Java properties file. + */ + public FileGuacamoleProperties(File propertiesFile) throws GuacamoleException { + super(read(propertiesFile)); + } + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/properties/GuacamoleProperties.java b/guacamole-ext/src/main/java/org/apache/guacamole/properties/GuacamoleProperties.java new file mode 100644 index 000000000..d77512687 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/properties/GuacamoleProperties.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.properties; + +import java.util.Properties; +import org.apache.guacamole.GuacamoleException; + +/** + * An arbitrary set of Guacamole configuration property name/value pairs. This + * interface is similar in concept to {@link Properties} except that + * implementations are not required to allow properties to be enumerated or + * iterated. Properties may simply be retrieved by their names, if known. + */ +public interface GuacamoleProperties { + + /** + * Returns the value of the property having the given name, if defined. If + * no such property exists, null is returned. + * + * @param name + * The name of the property to retrieve. + * + * @return + * The value of the given property, or null if no such property is + * defined. + * + * @throws GuacamoleException + * If an error prevents the given property from being read. + */ + String getProperty(String name) throws GuacamoleException; + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/properties/PropertiesGuacamoleProperties.java b/guacamole-ext/src/main/java/org/apache/guacamole/properties/PropertiesGuacamoleProperties.java new file mode 100644 index 000000000..263f7ee3e --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/properties/PropertiesGuacamoleProperties.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.properties; + +import java.util.Properties; +import org.apache.guacamole.GuacamoleException; + +/** + * GuacamoleProperties implementation which reads all properties from a + * {@link Properties} object. + */ +public class PropertiesGuacamoleProperties implements GuacamoleProperties { + + /** + * The Properties from which all property values should be read. + */ + private final Properties properties; + + /** + * Creates a new PropertiesGuacamoleProperties which wraps the given + * {@link Properties}, providing access to the values of any properties + * defined therein. + * + * @param properties + * The Properties that should be used as the source of all property + * values exposed by this instance of PropertiesGuacamoleProperties. + */ + public PropertiesGuacamoleProperties(Properties properties) { + this.properties = properties; + } + + @Override + public String getProperty(String name) throws GuacamoleException { + return properties.getProperty(name); + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java index f793575e6..efe6943e3 100644 --- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java @@ -24,15 +24,17 @@ import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Stage; import com.google.inject.servlet.GuiceServletContextListener; +import java.io.File; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; import javax.servlet.ServletContextEvent; import org.apache.guacamole.environment.Environment; -import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.extension.ExtensionModule; import org.apache.guacamole.log.LogModule; import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.properties.BooleanGuacamoleProperty; +import org.apache.guacamole.properties.FileGuacamoleProperties; import org.apache.guacamole.rest.RESTServiceModule; import org.apache.guacamole.rest.auth.HashTokenSessionMap; import org.apache.guacamole.rest.auth.TokenSessionMap; @@ -86,6 +88,18 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener */ private final Logger logger = LoggerFactory.getLogger(GuacamoleServletContextListener.class); + /** + * A property that determines whether environment variables are evaluated + * to override properties specified in guacamole.properties. + */ + private static final BooleanGuacamoleProperty ENABLE_ENVIRONMENT_PROPERTIES = + new BooleanGuacamoleProperty() { + @Override + public String getName() { + return "enable-environment-properties"; + } + }; + /** * The Guacamole server environment. */ @@ -111,16 +125,34 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener @Override public void contextInitialized(ServletContextEvent servletContextEvent) { + environment = new WebApplicationEnvironment(); + + // Read configuration information from GUACAMOLE_HOME/guacamole.properties try { - environment = new LocalEnvironment(); - sessionMap = new HashTokenSessionMap(environment); + environment.addGuacamoleProperties(new FileGuacamoleProperties( + new File(environment.getGuacamoleHome(), "guacamole.properties"))); } catch (GuacamoleException e) { logger.error("Unable to read guacamole.properties: {}", e.getMessage()); logger.debug("Error reading guacamole.properties.", e); - throw new RuntimeException(e); } + // For any values not defined in GUACAMOLE_HOME/guacamole.properties, + // read from system environment if "enable-environment-properties" is + // set to "true" + try { + if (environment.getProperty(ENABLE_ENVIRONMENT_PROPERTIES, false)) + environment.addGuacamoleProperties(new SystemEnvironmentGuacamoleProperties()); + } + catch (GuacamoleException e) { + logger.error("Unable to configure support for environment properties: {}", e.getMessage()); + logger.debug("Error reading \"{}\" property from guacamole.properties.", ENABLE_ENVIRONMENT_PROPERTIES.getName(), e); + } + + // Now that at least the main guacamole.properties source of + // configuration information is available, initialize the session map + sessionMap = new HashTokenSessionMap(environment); + // NOTE: The superclass implementation of contextInitialized() is // expected to invoke getInjector(), hence the need to call AFTER // setting up the environment and session map diff --git a/guacamole/src/main/java/org/apache/guacamole/SystemEnvironmentGuacamoleProperties.java b/guacamole/src/main/java/org/apache/guacamole/SystemEnvironmentGuacamoleProperties.java new file mode 100644 index 000000000..7ad2fd466 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/SystemEnvironmentGuacamoleProperties.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole; + +import org.apache.guacamole.properties.GuacamoleProperties; +import org.apache.guacamole.token.TokenName; + +/** + * GuacamoleProperties implementation which reads all properties from + * environment variables. The name of the environment variable corresponding to + * any particular property is determined using + * {@link TokenName#canonicalize(java.lang.String)}. + */ +public class SystemEnvironmentGuacamoleProperties implements GuacamoleProperties { + + @Override + public String getProperty(String name) { + return System.getenv(TokenName.canonicalize(name)); + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/WebApplicationEnvironment.java b/guacamole/src/main/java/org/apache/guacamole/WebApplicationEnvironment.java new file mode 100644 index 000000000..9f33a8136 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/WebApplicationEnvironment.java @@ -0,0 +1,364 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import org.codehaus.jackson.map.ObjectMapper; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration; +import org.apache.guacamole.properties.GuacamoleProperties; +import org.apache.guacamole.properties.GuacamoleProperty; +import org.apache.guacamole.protocols.ProtocolInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The environment of the locally-running instance of the Guacamole web + * application. + */ +public class WebApplicationEnvironment implements Environment { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(WebApplicationEnvironment.class); + + /** + * Array of all known protocol names. + */ + private static final String[] KNOWN_PROTOCOLS = new String[] { + "kubernetes", + "rdp", + "ssh", + "telnet", + "vnc", + }; + + /** + * The hostname to use when connecting to guacd if no hostname is provided + * within guacamole.properties. + */ + private static final String DEFAULT_GUACD_HOSTNAME = "localhost"; + + /** + * The port to use when connecting to guacd if no port is provided within + * guacamole.properties. + */ + private static final int DEFAULT_GUACD_PORT = 4822; + + /** + * Whether SSL/TLS is enabled for connections to guacd if not specified + * within guacamole.properties. + */ + private static final boolean DEFAULT_GUACD_SSL = false; + + /** + * The location of GUACAMOLE_HOME, which may not truly exist. + */ + private final File guacHome; + + /** + * The map of all available protocols. + */ + private final Map availableProtocols; + + /** + * All GuacamoleProperties instances added via addGuacamoleProperties(), in + * the order that they were added. + */ + private final List availableProperties = new CopyOnWriteArrayList<>(); + + /** + * The Jackson parser for parsing JSON files. + */ + private static final ObjectMapper mapper = new ObjectMapper(); + + /** + * Creates a new Environment, initializing that environment based on the + * location of GUACAMOLE_HOME and the contents of guacamole.properties. + */ + public WebApplicationEnvironment() { + + // Determine location of GUACAMOLE_HOME + guacHome = findGuacamoleHome(); + logger.info("GUACAMOLE_HOME is \"{}\".", guacHome.getAbsolutePath()); + + // Read all protocols + availableProtocols = readProtocols(); + + } + + /** + * Locates the Guacamole home directory by checking, in order: + * the guacamole.home system property, the GUACAMOLE_HOME environment + * variable, and finally the .guacamole directory in the home directory of + * the user running the servlet container. If even the .guacamole directory + * doesn't exist, then /etc/guacamole will be used. + * + * @return The File representing the Guacamole home directory, which may + * or may not exist, and may turn out to not be a directory. + */ + private static File findGuacamoleHome() { + + // Attempt to find Guacamole home + File guacHome; + + // Use system property by default + String desiredDir = System.getProperty("guacamole.home"); + + // Failing that, try the GUACAMOLE_HOME environment variable + if (desiredDir == null) desiredDir = System.getenv("GUACAMOLE_HOME"); + + // If successful, use explicitly specified directory + if (desiredDir != null) + guacHome = new File(desiredDir); + + // If not explicitly specified, use standard locations + else { + + // Try ~/.guacamole first + guacHome = new File(System.getProperty("user.home"), ".guacamole"); + + // If that doesn't exist, try /etc/guacamole if the /etc directory + // exists on this system + if (!guacHome.exists() && new File("/etc").exists()) + guacHome = new File("/etc/guacamole"); + + } + + // Return discovered directory + return guacHome; + + } + + /** + * Parses the given JSON file, returning the parsed ProtocolInfo. The JSON + * format is conveniently and intentionally identical to a serialized + * ProtocolInfo object, which is identical to the JSON format used by the + * protocol REST service built into the Guacamole web application. + * + * @param input + * An input stream containing JSON describing the forms and parameters + * associated with a protocol supported by Guacamole. + * + * @return + * A new ProtocolInfo object which contains the forms and parameters + * described by the JSON file parsed. + * + * @throws IOException + * If an error occurs while parsing the JSON file. + */ + private ProtocolInfo readProtocol(InputStream input) + throws IOException { + return mapper.readValue(input, ProtocolInfo.class); + } + + /** + * Reads through all pre-defined protocols and any protocols within the + * "protocols" subdirectory of GUACAMOLE_HOME, returning a map containing + * each of these protocols. The key of each entry will be the name of that + * protocol, as would be passed to guacd during connection. + * + * @return + * A map of all available protocols. + */ + private Map readProtocols() { + + // Map of all available protocols + Map protocols = new HashMap(); + + // Get protcols directory + File protocol_directory = new File(getGuacamoleHome(), "protocols"); + + // Read protocols from directory if it exists + if (protocol_directory.isDirectory()) { + + // Get all JSON files + File[] files = protocol_directory.listFiles( + new FilenameFilter() { + + @Override + public boolean accept(File file, String string) { + return string.endsWith(".json"); + } + + } + ); + + // Warn if directory contents are not available + if (files == null) { + logger.error("Unable to read contents of \"{}\".", protocol_directory.getAbsolutePath()); + files = new File[0]; + } + + // Load each protocol from each file + for (File file : files) { + + try { + + // Parse protocol + FileInputStream stream = new FileInputStream(file); + ProtocolInfo protocol = readProtocol(stream); + stream.close(); + + // Store protocol + protocols.put(protocol.getName(), protocol); + + } + catch (IOException e) { + logger.error("Unable to read connection parameter information from \"{}\": {}", file.getAbsolutePath(), e.getMessage()); + logger.debug("Error reading protocol JSON.", e); + } + + } + + } + + // If known protocols are not already defined, read from classpath + for (String protocol : KNOWN_PROTOCOLS) { + + // If protocol not defined yet, attempt to load from classpath + if (!protocols.containsKey(protocol)) { + + InputStream stream = WebApplicationEnvironment.class.getResourceAsStream( + "/org/apache/guacamole/protocols/" + + protocol + ".json"); + + // Parse JSON if available + if (stream != null) { + try { + protocols.put(protocol, readProtocol(stream)); + } + catch (IOException e) { + logger.error("Unable to read pre-defined connection parameter information for protocol \"{}\": {}", protocol, e.getMessage()); + logger.debug("Error reading pre-defined protocol JSON.", e); + } + } + + } + + } + + // Protocols map now fully populated + return protocols; + + } + + @Override + public File getGuacamoleHome() { + return guacHome; + } + + /** + * Returns the string value of the property having the given name, trying + * each source of properties added with addGuacamoleProperties() until a + * value is found. If no such property is defined, null is returned. + * + * @param name + * The name of the property value to retrieve. + * + * @return + * The value of the property having the given name, or null if no such + * property is defined. + * + * @throws GuacamoleException + * If an error occurs retrieving a property from a GuacamoleProperties + * implementation added via addGuacamoleProperties(). + */ + private String getPropertyValue(String name) throws GuacamoleException { + + // Search all provided GuacamoleProperties implementations, in order + for (GuacamoleProperties properties : availableProperties) { + String value = properties.getProperty(name); + if (value != null) + return value; + } + + // No such property + return null; + + } + + @Override + public Type getProperty(GuacamoleProperty property) throws GuacamoleException { + return property.parseValue(getPropertyValue(property.getName())); + } + + @Override + public Type getProperty(GuacamoleProperty property, + Type defaultValue) throws GuacamoleException { + + Type value = getProperty(property); + if (value == null) + return defaultValue; + + return value; + + } + + @Override + public Type getRequiredProperty(GuacamoleProperty property) + throws GuacamoleException { + + Type value = getProperty(property); + if (value == null) + throw new GuacamoleServerException("Property " + property.getName() + " is required."); + + return value; + + } + + @Override + public Map getProtocols() { + return availableProtocols; + } + + @Override + public ProtocolInfo getProtocol(String name) { + return availableProtocols.get(name); + } + + @Override + public GuacamoleProxyConfiguration getDefaultGuacamoleProxyConfiguration() + throws GuacamoleException { + + // Parse guacd hostname/port/ssl properties + return new GuacamoleProxyConfiguration( + getProperty(Environment.GUACD_HOSTNAME, DEFAULT_GUACD_HOSTNAME), + getProperty(Environment.GUACD_PORT, DEFAULT_GUACD_PORT), + getProperty(Environment.GUACD_SSL, DEFAULT_GUACD_SSL) + ); + + } + + @Override + public void addGuacamoleProperties(GuacamoleProperties properties) { + availableProperties.add(properties); + } + +}