Merge patch branch changes to main.

This commit is contained in:
Virtually Nick committed 2025-12-12 20:37:37 -05:00
commit a6719b1093
61 files changed
+1320 -288

No files matched your search

+1
View File
@@ -1 +1,2 @@
src/main/frontend/dist/**/*
src/main/resources/META-INF/services/*
@@ -26,6 +26,7 @@ import javax.ws.rs.ApplicationPath;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.jvnet.hk2.guice.bridge.api.GuiceBridge;
import org.jvnet.hk2.guice.bridge.api.GuiceIntoHK2Bridge;
import org.slf4j.bridge.SLF4JBridgeHandler;
@@ -68,6 +69,10 @@ public class GuacamoleApplication extends ResourceConfig {
// Automatically scan for REST resources
packages("org.apache.guacamole.rest");
// Explicitly disable WADL generation (we don't use it and Jersey warns
// of the fact that it won't generate a WADL otherwise)
property(ServerProperties.WADL_FEATURE_DISABLE, true);
// Use Jackson for JSON
register(JacksonFeature.class);
@@ -0,0 +1,90 @@
/*
* 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 ch.qos.logback.core.joran.spi.JoranException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.apache.guacamole.log.GuacamoleLogbackServiceProvider;
import org.apache.guacamole.log.LogLevel;
import org.apache.guacamole.log.ReconfigurableLoggerFactory;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.Reporter;
/**
* ServletContainerInitializer which forces the SLF4J service provider to be
* Guacamole's built-in logging system. This is necessary as Logback also
* includes an SLF4J service provider, and there is no guarantee which provider
* will be chosen by SLF4J in the case that multiple are present.
* <p>
* SLF4J uses a "slf4j.provider" system property to allow a specific provider
* to be chosen, which this initializer sets automatically.
*/
public class GuacamoleServletContainerInitializer implements ServletContainerInitializer {
/**
* The log level that should be used when we have not yet loaded any
* configuration dictating otherwise.
*/
private static final LogLevel EARLY_LOG_LEVEL = LogLevel.INFO;
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
// Ideally, we want to use Guacamole's built-in logging facility, but
// we shouldn't override the administrator if they have their own SLF4J
// provider that needs to be used instead (ie: for some log aggregation
// service)
if (System.getProperty(LoggerFactory.PROVIDER_PROPERTY_KEY) == null) {
System.setProperty(LoggerFactory.PROVIDER_PROPERTY_KEY,
GuacamoleLogbackServiceProvider.class.getCanonicalName());
// Avoid excessive verbosity from SLF4J unless the administrator
// has explicitly chosen SLF4J's verbosity level (the SLF4J provider
// class will otherwise be logged at startup, which is an internal
// detail and not typically needed)
if (System.getProperty(Reporter.SLF4J_INTERNAL_VERBOSITY_KEY) == null)
System.setProperty(Reporter.SLF4J_INTERNAL_VERBOSITY_KEY, "WARN");
}
// Attempt early initialization of Logback (before we begin parsing
// Guacamole's configuration), such that even startup messages follow
// a consistent format
try (InputStream logbackConfiguration = EARLY_LOG_LEVEL.getLogbackConfiguration()) {
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
if (loggerFactory instanceof ReconfigurableLoggerFactory)
((ReconfigurableLoggerFactory) loggerFactory).reconfigure(logbackConfiguration);
}
catch (JoranException | IOException e) {
System.err.println("Early initialization of Logback failed - "
+ "logging format may not match until the web application "
+ "finishes starting up: " + e.getMessage());
}
}
}
@@ -166,8 +166,7 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener
logger.info("Read configuration parameters from \"{}\".", guacProperties);
}
catch (GuacamoleException e) {
logger.error("Unable to read guacamole.properties: {}", e.getMessage());
logger.debug("Error reading guacamole.properties.", e);
logger.error("Unable to read guacamole.properties: {}", e.getMessage(), e);
}
// For any values not defined in GUACAMOLE_HOME/guacamole.properties,
@@ -181,8 +180,7 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener
}
}
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);
logger.error("Unable to configure support for environment properties: {}", e.getMessage(), e);
}
// For any values not defined in GUACAMOLE_HOME/guacamole.properties
@@ -198,8 +196,7 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener
}
}
catch (GuacamoleException e) {
logger.error("Unable to configure support for file environment properties: {}", e.getMessage());
logger.debug("Error reading \"{}\" property from guacamole.properties.", ENABLE_FILE_ENVIRONMENT_PROPERTIES.getName(), e);
logger.error("Unable to configure support for file environment properties: {}", e.getMessage(), e);
}
// Now that at least the main guacamole.properties source of
@@ -225,8 +222,7 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener
});
}
catch (GuacamoleException e) {
logger.error("An extension listening for application startup failed: {}", e.getMessage());
logger.debug("Extension failed internally while handling the application startup event.", e);
logger.error("An extension listening for application startup failed: {}", e.getMessage(), e);
}
}
@@ -301,14 +297,15 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener
}
// Inform any listeners that application shutdown has completed
try {
listenerService.handleEvent(new ApplicationShutdownEvent() {
// The application shutdown event currently has no content
});
}
catch (GuacamoleException e) {
logger.error("An extension listening for application shutdown failed: {}", e.getMessage());
logger.debug("Extension failed internally while handling the application shutdown event.", e);
if (listenerService != null) {
try {
listenerService.handleEvent(new ApplicationShutdownEvent() {
// The application shutdown event currently has no content
});
}
catch (GuacamoleException e) {
logger.error("An extension listening for application shutdown failed: {}", e.getMessage(), e);
}
}
}
@@ -320,8 +320,7 @@ public class GuacamoleSession {
});
}
catch (GuacamoleException e) {
logger.error("An extension listening for session invalidation failed: {}", e.getMessage());
logger.debug("Extension failed internally while handling the session invalidation event.", e);
logger.error("An extension listening for session invalidation failed: {}", e.getMessage(), e);
}
}
@@ -51,8 +51,8 @@ public class SystemFileEnvironmentGuacamoleProperties implements GuacamoleProper
return Files.asCharSource(new File(filename), StandardCharsets.UTF_8).read();
}
catch (IOException e) {
logger.error("Property \"{}\" could not be read from file \"{}\": {}", name, filename, e.getMessage());
logger.debug("Error reading property value from file.", e);
logger.error("Property \"{}\" could not be read from file \"{}\": {}", name,
filename, e.getMessage(), e);
}
}
@@ -106,13 +106,13 @@ public class FileAuthenticationProvider extends SimpleAuthenticationProvider {
parser = SAXParserFactory.newInstance().newSAXParser();
}
catch (ParserConfigurationException e) {
logger.error("Unable to create XML parser for reading \"{}\": {}", USER_MAPPING_FILENAME, e.getMessage());
logger.debug("An instance of SAXParser could not be created.", e);
logger.error("Unable to create XML parser for reading \"{}\": {}",
USER_MAPPING_FILENAME, e.getMessage(), e);
return null;
}
catch (SAXException e) {
logger.error("Unable to create XML parser for reading \"{}\": {}", USER_MAPPING_FILENAME, e.getMessage());
logger.debug("An instance of SAXParser could not be created.", e);
logger.error("Unable to create XML parser for reading \"{}\": {}",
USER_MAPPING_FILENAME, e.getMessage(), e);
return null;
}
@@ -138,15 +138,13 @@ public class FileAuthenticationProvider extends SimpleAuthenticationProvider {
// If the file is unreadable, return no mapping
catch (IOException e) {
logger.warn("Unable to read user mapping file \"{}\": {}", userMappingFile, e.getMessage());
logger.debug("Error reading user mapping file.", e);
logger.warn("Unable to read user mapping file \"{}\": {}", userMappingFile, e.getMessage(), e);
return null;
}
// If the file cannot be parsed, return no mapping
catch (SAXException e) {
logger.warn("User mapping file \"{}\" is not valid: {}", userMappingFile, e.getMessage());
logger.debug("Error parsing user mapping file.", e);
logger.warn("User mapping file \"{}\" is not valid: {}", userMappingFile, e.getMessage(), e);
return null;
}
@@ -183,30 +183,33 @@ public class EventLoggingListener implements Listener {
if (creds.isEmpty())
logger.debug("Empty authentication attempt (login screen "
+ "initialization) from {} failed: {}",
new RemoteAddress(creds), new Failure(event));
new RemoteAddress(creds), new Failure(event), event.getFailure());
else if (username == null || username.isEmpty())
logger.debug("Anonymous authentication attempt from {} failed: {}",
new RemoteAddress(creds), new Failure(event));
new RemoteAddress(creds), new Failure(event), event.getFailure());
else if (event.getFailure() instanceof GuacamoleInsufficientCredentialsException) {
if (authProvider != null)
logger.debug("Authentication attempt from {} for user \"{}\" "
+ "requires additional credentials to continue: {} "
+ "(requested by \"{}\")", new RemoteAddress(creds),
username, new Failure(event), authProvider.getIdentifier());
username, new Failure(event), authProvider.getIdentifier(),
event.getFailure());
else
logger.debug("Authentication attempt from {} for user \"{}\" "
+ "requires additional credentials to continue: {}",
new RemoteAddress(creds), username, new Failure(event));
new RemoteAddress(creds), username, new Failure(event),
event.getFailure());
}
else {
if (authProvider != null)
logger.warn("Authentication attempt from {} for user \"{}\" "
+ "failed: {} (rejected by \"{}\")", new RemoteAddress(creds),
username, new Failure(event), authProvider.getIdentifier());
username, new Failure(event), authProvider.getIdentifier(),
event.getFailure());
else
logger.warn("Authentication attempt from {} for user \"{}\" "
+ "failed: {}", new RemoteAddress(creds), username,
new Failure(event));
new Failure(event), event.getFailure());
}
}
@@ -40,7 +40,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
/**
* Logger for this class.
*/
private Logger logger = LoggerFactory.getLogger(AuthenticationProviderFacade.class);
private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderFacade.class);
/**
* The underlying authentication provider, or null if the authentication
@@ -86,9 +86,11 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
public AuthenticationProviderFacade(
Class<? extends AuthenticationProvider> authProviderClass,
Set<String> tolerateFailures) {
this.tolerateFailures = tolerateFailures;
this.authProvider = ProviderFactory.newInstance("authentication provider",
authProviderClass);
}
@Override
@@ -155,15 +157,11 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
* authentication provider being skipped.
*/
private void warnAuthProviderSkipped(Throwable e) {
logger.warn("The \"{}\" authentication provider has been skipped due "
+ "to an internal error. If this is unexpected or you are the "
+ "developer of this authentication provider, you may wish to "
+ "enable debug-level logging: {}",
getIdentifier(), e.getMessage());
logger.debug("Authentication provider skipped due to an internal failure.", e);
getIdentifier(), e.getMessage(), e);
}
/**
@@ -299,7 +297,7 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
// Delegate to underlying auth provider
return authProvider.updateUserContext(context, authenticatedUser, credentials);
}
@Override
@@ -32,7 +32,7 @@ import org.apache.guacamole.GuacamoleException;
* A ClassLoader implementation which finds classes within .jar files within a
* given directory.
*/
public class DirectoryClassLoader extends URLClassLoader {
public class DirectoryClassLoader extends URLClassLoader implements LoggerContextProvider {
/**
* Returns all .jar files within the given directory as an array of URLs.
@@ -105,4 +105,9 @@ public class DirectoryClassLoader extends URLClassLoader {
super(getJarURLs(dir), DirectoryClassLoader.class.getClassLoader());
}
@Override
public String getLoggerContext() {
return "lib";
}
}
@@ -397,7 +397,7 @@ public class Extension {
}
// Create isolated classloader for this extension
classLoader = new ExtensionClassLoader(file, temporaryFiles, parent);
classLoader = new ExtensionClassLoader(manifest, file, temporaryFiles, parent);
}
@@ -46,7 +46,7 @@ import org.slf4j.LoggerFactory;
* the given .jar. If classes are defined in both the parent and the extension
* .jar, the versions defined within the extension .jar are used.
*/
public class ExtensionClassLoader extends URLClassLoader {
public class ExtensionClassLoader extends URLClassLoader implements LoggerContextProvider {
/**
* Logger for this class.
@@ -71,6 +71,11 @@ public class ExtensionClassLoader extends URLClassLoader {
*/
private final ClassLoader parent;
/**
* The parsed manifest file of the extension that this class loader is for.
*/
private final ExtensionManifest manifest;
/**
* Returns the URL that refers to the given file. If the given file refers
* to a directory, an exception is thrown.
@@ -251,6 +256,9 @@ public class ExtensionClassLoader extends URLClassLoader {
* multiple times will not affect previously-returned instances of
* ExtensionClassLoader.
*
* @param manifest
* The parsed manifest of the extension.
*
* @param extension
* The extension .jar file from which classes should be loaded.
*
@@ -267,10 +275,11 @@ public class ExtensionClassLoader extends URLClassLoader {
* If the given file is not actually a file, or the contents of the
* file cannot be read.
*/
public ExtensionClassLoader(File extension, List<File> temporaryFiles,
ClassLoader parent) throws GuacamoleException {
public ExtensionClassLoader(ExtensionManifest manifest, File extension,
List<File> temporaryFiles, ClassLoader parent) throws GuacamoleException {
super(getExtensionURLs(extension, temporaryFiles), null);
this.parent = parent;
this.manifest = manifest;
}
@Override
@@ -288,4 +297,9 @@ public class ExtensionClassLoader extends URLClassLoader {
}
@Override
public String getLoggerContext() {
return "ext:" + manifest.getNamespace();
}
}
@@ -434,8 +434,9 @@ public class ExtensionModule extends ServletModule {
// Use empty set by default if property cannot be parsed
catch (GuacamoleException e) {
logger.warn("The list of authentication providers specified via the \"{}\" property could not be parsed: {}", SKIP_IF_UNAVAILABLE.getName(), e.getMessage());
logger.debug("Unable to parse \"{}\" property.", SKIP_IF_UNAVAILABLE.getName(), e);
logger.warn("The list of authentication providers specified via "
+ "the \"{}\" property could not be parsed: {}",
SKIP_IF_UNAVAILABLE.getName(), e.getMessage(), e);
return Collections.<String>emptySet();
}
@@ -457,8 +458,9 @@ public class ExtensionModule extends ServletModule {
// Sort by filename if the desired order cannot be read
catch (GuacamoleException e) {
logger.warn("The list of extensions specified via the \"{}\" property could not be parsed: {}", EXTENSION_PRIORITY.getName(), e.getMessage());
logger.debug("Unable to parse \"{}\" property.", EXTENSION_PRIORITY.getName(), e);
logger.warn("The list of extensions specified via the \"{}\" "
+ "property could not be parsed: {}",
EXTENSION_PRIORITY.getName(), e.getMessage(), e);
return ExtensionOrderProperty.DEFAULT_COMPARATOR;
}
@@ -522,8 +524,8 @@ public class ExtensionModule extends ServletModule {
}
catch (GuacamoleException e) {
logger.error("Extension \"{}\" could not be loaded: {}", extensionFile.getName(), e.getMessage());
logger.debug("Unable to load extension.", e);
logger.error("Extension \"{}\" could not be loaded: {}",
extensionFile.getName(), e.getMessage(), e);
}
}
@@ -125,8 +125,7 @@ public class LanguageResourceService {
// Warn of failure to parse
catch (GuacamoleException e) {
parsedAllowedLanguages = null;
logger.error("Unable to parse list of allowed languages: {}", e.getMessage());
logger.debug("Error parsing list of allowed languages.", e);
logger.error("Unable to parse list of allowed languages: {}", e.getMessage(), e);
}
this.allowedLanguages = parsedAllowedLanguages;
@@ -308,8 +307,7 @@ public class LanguageResourceService {
}
catch (IOException e) {
logger.error("Unable to merge language resource \"{}\": {}", key, e.getMessage());
logger.debug("Error merging language resource.", e);
logger.error("Unable to merge language resource \"{}\": {}", key, e.getMessage(), e);
}
}
@@ -437,8 +435,7 @@ public class LanguageResourceService {
// Continue with next language if unable to read
catch (IOException e) {
logger.warn("Unable to read language resource \"{}\".", languageKey);
logger.debug("Error reading language resource.", e);
logger.warn("Unable to read language resource \"{}\".", languageKey, e);
}
}
@@ -0,0 +1,41 @@
/*
* 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.extension;
/**
* Provider of general, human-readable text that describes the context of a
* logger. This is currently used to allow our various custom class loaders to
* provide contextual information, which our custom logger factory can pull from
* the call stack when loggers are created.
*/
public interface LoggerContextProvider {
/**
* Returns human-readable text describing the context of events logged by
* a logger. The text should describe the general context of the source of
* the entry.
*
* @return
* Human-readable text describing the context of events logged by any
* logger created/maintained by the class implementing this interface.
*/
String getLoggerContext();
}
@@ -61,35 +61,29 @@ class ProviderFactory {
catch (NoSuchMethodException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("{} is missing a default constructor.",
providerClass.getName(), e);
+ "are the developer, turn on debug-level logging.",
typeName, e);
}
catch (SecurityException e) {
logger.error("The Java security manager is preventing extensions "
+ "from being loaded. Please check the configuration of Java or your "
+ "servlet container.");
logger.debug("Creation of {} disallowed by security manager.",
providerClass.getName(), e);
+ "servlet container.", e);
}
catch (InstantiationException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("{} cannot be instantiated.", providerClass.getName(), e);
+ "are the developer, turn on debug-level logging.",
typeName, e);
}
catch (IllegalAccessException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.");
logger.debug("Default constructor of {} is not public.", typeName, e);
+ "are the developer, turn on debug-level logging.", e);
}
catch (IllegalArgumentException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("Default constructor of {} cannot accept zero arguments.",
providerClass.getName(), e);
+ "are the developer, turn on debug-level logging.", typeName, e);
}
catch (InvocationTargetException e) {
// Obtain causing error - create relatively-informative stub error if cause is unknown
@@ -97,8 +91,7 @@ class ProviderFactory {
if (cause == null)
cause = new GuacamoleException("Error encountered during initialization.");
logger.error("{} extension failed to start: {}", typeName, cause.getMessage());
logger.debug("{} instantiation failed.", providerClass.getName(), e);
logger.error("{} extension failed to start: {}", typeName, cause.getMessage(), cause);
}
return instance;
@@ -0,0 +1,429 @@
/*
* 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.log;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.slf4j.event.Level;
import org.slf4j.spi.LoggingEventBuilder;
/**
* {@link Logger} that automatically includes arbitrary context. The additional
* context is included as a {@link Marker}, and all logging functions are
* overridden to include at least this Marker. If a logging function is given a
* Marker by the caller, it is wrapped as a reference of Marker created by the
* ContextAwareLogger's Marker.
*/
public class ContextAwareLogger implements Logger {
/**
* The wrapped Logger.
*/
private final Logger logger;
/**
* The arbitrary context that should be included as a Marker in all log
* messages.
*/
private final String context;
/**
* Creates a new ContextAwareLogger that wraps given Logger, automatically
* including the provided context as a Marker for each logged message.
*
* @param context
* The arbitrary context to include as a Marker with each
* logged message.
*
* @param logger
* The Logger to wrap.
*/
public ContextAwareLogger(String context, Logger logger) {
this.logger = logger;
this.context = context;
}
/**
* Returns a Marker containing the arbitrary context provided when this
* ContextAwareLogger was created. If Markers are provided to this function,
* they are included as references within the returned Marker.
*
* @param refs
* The Markers to include in the returned Marker as references, if any.
*
* @return
* A Marker containing the context provided when this
* ContextAwareLogger was created, as well as any provided Marker
* references.
*/
private Marker getContextMarker(Marker... refs) {
Marker marker = MarkerFactory.getMarker(context);
Arrays.stream(refs).forEach(marker::add);
return marker;
}
@Override
public String getName() {
return logger.getName();
}
@Override
public LoggingEventBuilder makeLoggingEventBuilder(Level level) {
return logger.makeLoggingEventBuilder(level);
}
@Override
public LoggingEventBuilder atLevel(Level level) {
return logger.atLevel(level);
}
@Override
public boolean isEnabledForLevel(Level level) {
return logger.isEnabledForLevel(level);
}
@Override
public boolean isTraceEnabled() {
return logger.isTraceEnabled();
}
@Override
public void trace(String msg) {
logger.trace(getContextMarker(), msg);
}
@Override
public void trace(String format, Object arg) {
logger.trace(getContextMarker(), format, arg);
}
@Override
public void trace(String format, Object arg1, Object arg2) {
logger.trace(getContextMarker(), format, arg1, arg2);
}
@Override
public void trace(String format, Object... arguments) {
logger.trace(getContextMarker(), format, arguments);
}
@Override
public void trace(String msg, Throwable t) {
logger.trace(getContextMarker(), msg, t);
}
@Override
public boolean isTraceEnabled(Marker marker) {
return logger.isTraceEnabled(marker);
}
@Override
public LoggingEventBuilder atTrace() {
return logger.atTrace();
}
@Override
public void trace(Marker marker, String msg) {
logger.trace(getContextMarker(marker), msg);
}
@Override
public void trace(Marker marker, String format, Object arg) {
logger.trace(getContextMarker(marker), format, arg);
}
@Override
public void trace(Marker marker, String format, Object arg1, Object arg2) {
logger.trace(getContextMarker(marker), format, arg1, arg2);
}
@Override
public void trace(Marker marker, String format, Object... argArray) {
logger.trace(getContextMarker(marker), format, argArray);
}
@Override
public void trace(Marker marker, String msg, Throwable t) {
logger.trace(getContextMarker(marker), msg, t);
}
@Override
public boolean isDebugEnabled() {
return logger.isDebugEnabled();
}
@Override
public void debug(String msg) {
logger.debug(getContextMarker(), msg);
}
@Override
public void debug(String format, Object arg) {
logger.debug(getContextMarker(), format, arg);
}
@Override
public void debug(String format, Object arg1, Object arg2) {
logger.debug(getContextMarker(), format, arg1, arg2);
}
@Override
public void debug(String format, Object... arguments) {
logger.debug(getContextMarker(), format, arguments);
}
@Override
public void debug(String msg, Throwable t) {
logger.debug(getContextMarker(), msg, t);
}
@Override
public boolean isDebugEnabled(Marker marker) {
return logger.isDebugEnabled(marker);
}
@Override
public void debug(Marker marker, String msg) {
logger.debug(getContextMarker(marker), msg);
}
@Override
public void debug(Marker marker, String format, Object arg) {
logger.debug(getContextMarker(marker), format, arg);
}
@Override
public void debug(Marker marker, String format, Object arg1, Object arg2) {
logger.debug(getContextMarker(marker), format, arg1, arg2);
}
@Override
public void debug(Marker marker, String format, Object... arguments) {
logger.debug(getContextMarker(marker), format, arguments);
}
@Override
public void debug(Marker marker, String msg, Throwable t) {
logger.debug(getContextMarker(marker), msg, t);
}
@Override
public LoggingEventBuilder atDebug() {
return logger.atDebug();
}
@Override
public boolean isInfoEnabled() {
return logger.isInfoEnabled();
}
@Override
public void info(String msg) {
logger.info(getContextMarker(), msg);
}
@Override
public void info(String format, Object arg) {
logger.info(getContextMarker(), format, arg);
}
@Override
public void info(String format, Object arg1, Object arg2) {
logger.info(getContextMarker(), format, arg1, arg2);
}
@Override
public void info(String format, Object... arguments) {
logger.info(getContextMarker(), format, arguments);
}
@Override
public void info(String msg, Throwable t) {
logger.info(getContextMarker(), msg, t);
}
@Override
public boolean isInfoEnabled(Marker marker) {
return logger.isInfoEnabled(marker);
}
@Override
public void info(Marker marker, String msg) {
logger.info(getContextMarker(marker), msg);
}
@Override
public void info(Marker marker, String format, Object arg) {
logger.info(getContextMarker(marker), format, arg);
}
@Override
public void info(Marker marker, String format, Object arg1, Object arg2) {
logger.info(getContextMarker(marker), format, arg1, arg2);
}
@Override
public void info(Marker marker, String format, Object... arguments) {
logger.info(getContextMarker(marker), format, arguments);
}
@Override
public void info(Marker marker, String msg, Throwable t) {
logger.info(getContextMarker(marker), msg, t);
}
@Override
public LoggingEventBuilder atInfo() {
return logger.atInfo();
}
@Override
public boolean isWarnEnabled() {
return logger.isWarnEnabled();
}
@Override
public void warn(String msg) {
logger.warn(getContextMarker(), msg);
}
@Override
public void warn(String format, Object arg) {
logger.warn(getContextMarker(), format, arg);
}
@Override
public void warn(String format, Object... arguments) {
logger.warn(getContextMarker(), format, arguments);
}
@Override
public void warn(String format, Object arg1, Object arg2) {
logger.warn(getContextMarker(), format, arg1, arg2);
}
@Override
public void warn(String msg, Throwable t) {
logger.warn(getContextMarker(), msg, t);
}
@Override
public boolean isWarnEnabled(Marker marker) {
return logger.isWarnEnabled(marker);
}
@Override
public void warn(Marker marker, String msg) {
logger.warn(getContextMarker(marker), msg);
}
@Override
public void warn(Marker marker, String format, Object arg) {
logger.warn(getContextMarker(marker), format, arg);
}
@Override
public void warn(Marker marker, String format, Object arg1, Object arg2) {
logger.warn(getContextMarker(marker), format, arg1, arg2);
}
@Override
public void warn(Marker marker, String format, Object... arguments) {
logger.warn(getContextMarker(marker), format, arguments);
}
@Override
public void warn(Marker marker, String msg, Throwable t) {
logger.warn(getContextMarker(marker), msg, t);
}
@Override
public LoggingEventBuilder atWarn() {
return logger.atWarn();
}
@Override
public boolean isErrorEnabled() {
return logger.isErrorEnabled();
}
@Override
public void error(String msg) {
logger.error(getContextMarker(), msg);
}
@Override
public void error(String format, Object arg) {
logger.error(getContextMarker(), format, arg);
}
@Override
public void error(String format, Object arg1, Object arg2) {
logger.error(getContextMarker(), format, arg1, arg2);
}
@Override
public void error(String format, Object... arguments) {
logger.error(getContextMarker(), format, arguments);
}
@Override
public void error(String msg, Throwable t) {
logger.error(getContextMarker(), msg, t);
}
@Override
public boolean isErrorEnabled(Marker marker) {
return logger.isErrorEnabled(marker);
}
@Override
public void error(Marker marker, String msg) {
logger.error(getContextMarker(marker), msg);
}
@Override
public void error(Marker marker, String format, Object arg) {
logger.error(getContextMarker(marker), format, arg);
}
@Override
public void error(Marker marker, String format, Object arg1, Object arg2) {
logger.error(getContextMarker(marker), format, arg1, arg2);
}
@Override
public void error(Marker marker, String format, Object... arguments) {
logger.error(getContextMarker(marker), format, arguments);
}
@Override
public void error(Marker marker, String msg, Throwable t) {
logger.error(getContextMarker(marker), msg, t);
}
@Override
public LoggingEventBuilder atError() {
return logger.atError();
}
}
@@ -0,0 +1,355 @@
/*
* 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.log;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.util.ContextInitializer;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.guacamole.extension.LoggerContextProvider;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.MDC;
import org.slf4j.Marker;
import org.slf4j.spi.MDCAdapter;
/**
* {@link ILoggerFactory} implementation that automatically includes additional
* {@link Marker} context based on the origin of each log message. The origin
* of each message is determined by investigating the caller classes, with
* the actual context provided by the associated classloader if it implements
* {@link LoggerContextProvider}.
*/
public class ContextAwareLoggerFactory implements ReconfigurableLoggerFactory {
/**
* The name of the LoggerContext maintained by this logger factory.
*/
private static final String LOGGER_CONTEXT_NAME = "guacamole";
/**
* The default text that should be displayed if no other context can be
* derived from any caller.
*/
private static final String DEFAULT_CONTEXT = "app";
/**
* Regular expression that matches one or more newlines, regardless of style
* (Windows, Linux, etc.), preserving only the first such newline. This
* expression is intended to reliably match newlines that may be interpreted
* by the shell/viewer, regardless of the platform producing the log
* message.
* <p>
* This specific regular expression is intended for use with content that is
* being included inline (concatenated with an existing line of output).
*/
private static final String INLINE_NEWLINE_REGEX =
// Locate the right edge of the first newline sequence, whether
// that is CR, LF, CRLF, or LFCR
"(?<="
// Do not match newline sequences that are not the first
// newline in the sequence
+ "(?<![\\n\\r])"
// Match either CR, LF, CRLF, or LFCR, ensuring that CRLF
// does not get matched as just CR (same for LFCR vs. LF)
+ "("
+ "\\n(?!\\r)" // LF (with no following CR)
+ "|\\r(?!\\n)" // CR (with no following LF)
+ "|\\n\\r" // LFCR
+ "|\\r\\n" // CRLF
+ ")"
+ ")"
// Ignore (and strip) and remaining newline characters
+ "[\\n\\r]*";
/**
* Identical in purpose and behavior to {@link #INLINE_NEWLINE_REGEX}, but
* intended for output that stands on its own as a block of text. This
* variation preserves formatting of a block by matching the beginning of
* all lines (regardless of whether preceded by a newline) and not matching
* the final newline.
*/
private static final String BLOCK_NEWLINE_REGEX = "(^|" + INLINE_NEWLINE_REGEX + ")(?=[^\\r\\n])";
/**
* Logback encoder pattern fragment that produces a local timestamp (with
* timezone information) in roughly ISO 8601 format.
*/
private static final String LOG_PATTERN_FRAGMENT_TIMESTAMP = "%d{yyyy-MM-dd HH:mm:ss.SSS xxx}";
/**
* The text that should be prepended to log messages when they span multiple
* lines.
*/
private static final String CONTINUATION_PREFIX = "+ ";
/**
* Logback encoder pattern fragment that produces the logged message. This
* automatically prepends any newlines within the message with a "+ " prefix
* to clearly represent when a message has been split across multiple lines.
*/
private static final String LOG_PATTERN_FRAGMENT_MESSAGE_BODY = "%replace(%msg){'" + INLINE_NEWLINE_REGEX + "','" + CONTINUATION_PREFIX + "'}";
/**
* Logback encoder pattern that should be used for messages when the highest
* level of verbosity is not needed.
*/
private static final String LOG_PATTERN_DEFAULT = LOG_PATTERN_FRAGMENT_TIMESTAMP
+ " guacamole [%marker] %level: "
+ LOG_PATTERN_FRAGMENT_MESSAGE_BODY + "%n"
+ "%nopex";
/**
* Logback encoder pattern that should be used for messages with the highest
* level of verbosity.
*/
private static final String LOG_PATTERN_VERBOSE = LOG_PATTERN_FRAGMENT_TIMESTAMP
+ " guacamole [%marker, thread:%thread] %level: %logger{36} - "
+ LOG_PATTERN_FRAGMENT_MESSAGE_BODY + "%n"
+ "%replace(%xThrowable){'" + BLOCK_NEWLINE_REGEX + "','" + CONTINUATION_PREFIX + "'}";
/**
* The top-level Logback object, the LoggerContext, which is also Logback's
* LoggerFactory implementation.
*/
private final LoggerContext context;
/**
* Creates a new ContextAwareLoggerFactory that uses the given MDCAdapter
* for the {@link MDC} implementation.
*
* @param mdcAdapter
* The MDCAdapter that should be used to provide the MDC
* implementation.
*/
public ContextAwareLoggerFactory(MDCAdapter mdcAdapter) {
context = new LoggerContext();
context.setName(LOGGER_CONTEXT_NAME);
context.setMDCAdapter(mdcAdapter);
// Perform basic init (prior to loading any Guacamole-specific
// configuration, which is loaded by the LogModule)
try {
new ContextInitializer(context).autoConfig();
}
catch (JoranException | RuntimeException | Error e) {
System.err.println("Logging system could not be initialized: " + e.getMessage());
e.printStackTrace(System.err);
}
context.start();
}
/**
* Uses the {@link StackWalker} class provided by Java9+ to retrieve all
* classes currently on the call stack for the current function, in order of
* decreasing depth. The current function (the deepest function in terms of
* call depth) is the first item in the returned list.
* <p>
* NOTE: This method is only supported by Java 9 and late and will throw a
* ClassNotFoundException otherwise..
*
* @return
* A List of all classes currently on the call stack, in order of
* decreasing depth.
*
* @throws UnsupportedOperationException
* If support for StackWalker is not present (Java 8).
*/
@SuppressWarnings("unchecked")
private List<Class<?>> getCallersFromStackWalker()
throws UnsupportedOperationException {
//
// NOTE: The following is the reflection equivalent of:
//
// return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).walk((stack) ->
// stack.map((frame) -> frame.getDeclaringClass()).collect(Collectors.toList())
// );
//
try {
// Retrieve StackWalker definition and related classes
Class<?> stackWalkerClass = Class.forName("java.lang.StackWalker");
Class<?> stackFrameClass = Class.forName("java.lang.StackWalker$StackFrame");
Class<Enum> optionEnum = (Class<Enum>) Class.forName("java.lang.StackWalker$Option");
// Retrieve references to functions we will be needing
Method stackWalkerGetInstanceMethod = stackWalkerClass.getMethod("getInstance", optionEnum);
Method walkMethod = stackWalkerClass.getMethod("walk", Function.class);
Method getDeclaringClassMethod = stackFrameClass.getMethod("getDeclaringClass");
// Equivalent to:
// StackWalker stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
Object stackWalker = stackWalkerGetInstanceMethod.invoke(null,
Enum.valueOf(optionEnum, "RETAIN_CLASS_REFERENCE"));
// Equivalent to:
// (stack) -> stack.map((frame) -> frame.getDeclaringClass()).collect(Collectors.toList())
Function<Stream<?>, List<Class<?>>> walker =
(stack) -> stack.map((frame) -> {
try {
return (Class<?>) getDeclaringClassMethod.invoke(frame);
}
catch (ReflectiveOperationException e) {
throw new UnsupportedOperationException("getDeclaringClass() "
+ "could not be invoked for stack frame ", e);
}
}).collect(Collectors.toList());
// Equivalent to: stackWalker.walk(...)
return (List<Class<?>>) walkMethod.invoke(stackWalker, walker);
}
catch (ReflectiveOperationException e) {
throw new UnsupportedOperationException("StackWalker is not "
+ "supported by this version of Java", e);
}
}
/**
* Uses the deprecated {@link SecurityManager} to retrieve all classes
* currently on the call stack for the current function, in order of
* decreasing depth. The current function (the deepest function in terms of
* call depth) is the first item in the returned list.
* <p>
* NOTE: This is the only method that is supported by Java 8 that includes
* direct Class references.
*
* @return
* A List of all classes currently on the call stack, in order of
* decreasing depth.
*
* @throws UnsupportedOperationException
* If support for SecurityManager is not present (expected for future
* Java versions).
*/
@SuppressWarnings("removal")
private List<Class<?>> getCallersFromSecurityManager() {
try {
// Atempt to locate SecurityManager class and grab its
// getClassContext() method (which returns a stack trace)
Class<?> securityManagerClass = Class.forName("java.lang.SecurityManager");
Object securityManager = securityManagerClass.getDeclaredConstructor().newInstance();
Method getClassContextMethod = securityManagerClass.getDeclaredMethod("getClassContext");
// getClassContext() is protected
getClassContextMethod.setAccessible(true);
return Arrays.asList((Class<?>[]) getClassContextMethod.invoke(securityManager));
}
catch (ReflectiveOperationException e) {
throw new UnsupportedOperationException("SecurityManager is not "
+ "supported by this version of Java", e);
}
}
/**
* Returns all classes currently on the call stack for the current function,
* in order of decreasing depth. The current function (the deepest function
* in terms of call depth) is the first item in the returned list.
*
* @return
* A List of all classes currently on the call stack, in order of
* decreasing depth.
*
* @throws UnsupportedOperationException
* If no supported mechanism exists for retrieving the call stack.
*/
private List<Class<?>> getCallers() throws UnsupportedOperationException {
try {
return getCallersFromStackWalker();
}
catch (UnsupportedOperationException e) {
return getCallersFromSecurityManager();
}
}
/**
* Derives the context of the message currently being logged based on the
* content of the call stack.
*
* @return
* The context of the message currently being logged.
*/
private String getCallerLogContext() {
// Derive context from the most recent caller that explicitly provides
// their own logging context, if any
for (Class<?> caller : getCallers()) {
ClassLoader classloader = caller.getClassLoader();
if (classloader instanceof LoggerContextProvider)
return ((LoggerContextProvider) classloader).getLoggerContext();
}
// Use default context if no callers provide their own
return DEFAULT_CONTEXT;
}
@Override
public Logger getLogger(String name) {
return new ContextAwareLogger(getCallerLogContext(), context.getLogger(name));
}
@Override
public void reconfigure(InputStream logbackConfiguration) throws JoranException {
context.reset();
// Include convenience properties for building off Guacamole's
// built-in configuration with a custom logback.xml
context.putProperty("guac_timestamp_pattern", LOG_PATTERN_FRAGMENT_TIMESTAMP);
context.putProperty("guac_message_pattern", LOG_PATTERN_FRAGMENT_MESSAGE_BODY);
context.putProperty("guac_pattern", LOG_PATTERN_DEFAULT);
context.putProperty("guac_verbose_pattern", LOG_PATTERN_VERBOSE);
// Initialize logback
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
configurator.doConfigure(logbackConfiguration);
// Dump any errors that occur during logback init
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
}
}
@@ -0,0 +1,86 @@
/*
* 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.log;
import ch.qos.logback.classic.util.LogbackMDCAdapter;
import org.slf4j.IMarkerFactory;
import org.slf4j.helpers.BasicMarkerFactory;
import org.slf4j.spi.MDCAdapter;
import org.slf4j.spi.SLF4JServiceProvider;
/**
* SLF4JServiceProvider that overrides the default behavior of Logback's
* service provider, forcing the use of a custom, context-aware logger factory.
*/
public class GuacamoleLogbackServiceProvider implements SLF4JServiceProvider {
/**
* The highest version of SLF4J that this service provider can support.
* This is intentionally not actually a released version, but a reasonable
* upper bound. This value is identical to the value used by Logback.
*/
private static final String SLF4J_API_MAX_VERSION = "2.0.99";
/**
* The singleton instance of the LoggerFactory maintained by this service
* provider. The custom implementation provided here uses the call stack
* and our various custom class loaders to determine the context of the
* loggers that will be created.
*/
private ContextAwareLoggerFactory loggerFactory;
/**
* The singleton instance of the MDCAdapter implementation that should be
* used by Logback and SLF4J.
*/
private final MDCAdapter mdcAdapter = new LogbackMDCAdapter();
/**
* The singleton MarkerFactory instance maintained by this service
* provider.
*/
private final IMarkerFactory markerFactory = new BasicMarkerFactory();
@Override
public void initialize() {
loggerFactory = new ContextAwareLoggerFactory(mdcAdapter);
}
@Override
public MDCAdapter getMDCAdapter() {
return mdcAdapter;
}
@Override
public ContextAwareLoggerFactory getLoggerFactory() {
return loggerFactory;
}
@Override
public IMarkerFactory getMarkerFactory() {
return markerFactory;
}
@Override
public String getRequestedApiVersion() {
return SLF4J_API_MAX_VERSION;
}
}
@@ -56,14 +56,14 @@ public enum LogLevel {
* messages, while verbose, will not affect performance.
*/
@PropertyValue("debug")
DEBUG("debug"),
DEBUG("debug", "${guac_verbose_pattern}"),
/**
* Informational messages that may be useful for debugging, but which are
* so low-level that they may affect performance.
*/
@PropertyValue("trace")
TRACE("trace");
TRACE("trace", "${guac_verbose_pattern}");
/**
* Format string whose sole format argument is a String containing the
@@ -77,7 +77,7 @@ public enum LogLevel {
+ " <!-- Default appender -->\n"
+ " <appender name=\"GUAC-DEFAULT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n"
+ " <encoder>\n"
+ " <pattern>%%d{HH:mm:ss.SSS} [%%thread] %%-5level %%logger{36} - %%msg%%n</pattern>\n"
+ " <pattern>%s</pattern>\n"
+ " </encoder>\n"
+ " </appender>\n"
+ "\n"
@@ -102,9 +102,10 @@ public enum LogLevel {
private final byte[] logbackConfig;
/**
* Creates a new LogLevel with the given names. The pair of names provided
* correspond to the name used within Guacamole's configuration and the
* name used within Logback's configuration.
* Creates a new LogLevel with the given names and pattern. The pair of
* names provided correspond to the name used within Guacamole's
* configuration and the name used within Logback's configuration, while
* the pattern determines how Logback will convert log messages to text.
*
* @param canonicalName
* The name that should be used for this log level when configuring
@@ -113,10 +114,14 @@ public enum LogLevel {
* @param logbackLogLevel
* The name that would be provided to Logback to log at this level if
* manually configuring Logback using "logback.xml".
*
* @param pattern
* The Logback encoder pattern that should be use for messages logged
* at this level.
*/
private LogLevel(String canonicalName, String logbackLogLevel) {
private LogLevel(String canonicalName, String logbackLogLevel, String pattern) {
this.canonicalName = canonicalName;
this.logbackConfig = String.format(LOGBACK_XML_TEMPLATE, logbackLogLevel).getBytes(StandardCharsets.UTF_8);
this.logbackConfig = String.format(LOGBACK_XML_TEMPLATE, pattern, logbackLogLevel).getBytes(StandardCharsets.UTF_8);
}
/**
@@ -131,7 +136,27 @@ public enum LogLevel {
* "logback.xml" configuration file.
*/
private LogLevel(String logLevel) {
this(logLevel, logLevel);
this(logLevel, logLevel, "${guac_pattern}");
}
/**
* Creates a new LogLevel with the given name and pattern. The provided name
* corresponds to both the name used within Guacamole's configuration and
* the name used within Logback's configuration, while the pattern
* determines how Logback will convert log messages to text.
*
* @param logLevel
* The name that should be used for this log level when configuring
* Guacamole to log at this level using the "log-level" property AND
* when manually configuring Logback to log at this level using a
* "logback.xml" configuration file.
*
* @param pattern
* The Logback encoder pattern that should be use for messages logged
* at this level.
*/
private LogLevel(String logLevel, String pattern) {
this(logLevel, logLevel, pattern);
}
/**
@@ -19,10 +19,7 @@
package org.apache.guacamole.log;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import com.google.inject.AbstractModule;
import java.io.File;
import java.io.FileInputStream;
@@ -32,6 +29,7 @@ import java.io.InputStream;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.EnumGuacamoleProperty;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -97,7 +95,7 @@ public class LogModule extends AbstractModule {
}
catch (FileNotFoundException e) {
logger.info("Logback configuration could not be read "
+ "from \"{}\": {}", logbackFile, e.getMessage());
+ "from \"{}\": {}", logbackFile, e.getMessage(), e);
}
}
@@ -110,7 +108,7 @@ public class LogModule extends AbstractModule {
}
catch (GuacamoleException e) {
level = LogLevel.INFO;
logger.error("Falling back to \"{}\" log level: {}", level.getCanonicalName(), e.getMessage());
logger.error("Falling back to \"{}\" log level: {}", level.getCanonicalName(), e.getMessage(), e);
}
return level.getLogbackConfiguration();
@@ -122,25 +120,26 @@ public class LogModule extends AbstractModule {
try (InputStream logbackConfiguration = getLogbackConfiguration()) {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.reset();
// Warn if the system is somehow preventing Guacamole from
// using its own logging
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
if (!(loggerFactory instanceof ReconfigurableLoggerFactory)) {
logger.warn("Guacamole depends on its own SLF4J service "
+ "provider to configure logging, but your system has "
+ "forced a different service provider to take "
+ "priority. Guacamole's own logging configuration "
+ "may not have any effect, and logging context may "
+ "be missing.");
}
// Initialize logback
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
configurator.doConfigure(logbackConfiguration);
// Dump any errors that occur during logback init
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
((ReconfigurableLoggerFactory) loggerFactory).reconfigure(logbackConfiguration);
}
catch (JoranException e) {
logger.error("Initialization of logback failed: {}", e.getMessage());
logger.debug("Unable to load logback configuration.", e);
logger.error("Initialization of logback failed: {}", e.getMessage(), e);
}
catch (IOException e) {
logger.warn("Logback configuration file could not be cleanly closed: {}", e.getMessage());
logger.debug("Failed to close logback configuration file.", e);
logger.warn("Logback configuration file could not be cleanly closed: {}", e.getMessage(), e);
}
}
@@ -0,0 +1,46 @@
/*
* 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.log;
import ch.qos.logback.core.joran.spi.JoranException;
import java.io.InputStream;
import org.slf4j.ILoggerFactory;
/**
* An {@link ILoggerFactory} that can be reconfigured as needed.
*/
public interface ReconfigurableLoggerFactory extends ILoggerFactory {
/**
* Reconfigures this {@link ILoggerFactory} using the given InputStream for
* a Logback XML configuration file. It is the responsibility of the caller
* to close the provided InputStream.
*
* @param logbackConfiguration
* An InputStream that provides the desired Logback configuration in
* XML format.
*
* @throws JoranException
* If the configuration is invalid, or an error occurs that prevents
* the configuration from being read/applied.
*/
void reconfigure(InputStream logbackConfiguration) throws JoranException;
}
@@ -75,7 +75,7 @@ public class RESTExceptionMapper implements ExceptionMapper<Throwable> {
if (t instanceof GuacamoleUnauthorizedException) {
String token = authenticationService.getAuthenticationToken(request);
if (authenticationService.destroyGuacamoleSession(token))
logger.debug("Implicitly invalidated session for token \"{}\"", token);
logger.debug("Implicitly invalidated session for token \"{}\"", token, t);
}
// Translate GuacamoleException subclasses to HTTP error codes
@@ -84,11 +84,9 @@ public class RESTExceptionMapper implements ExceptionMapper<Throwable> {
// Always log the human-readable details of GuacacamoleExceptions
// for the benefit of the administrator
if (t instanceof GuacamoleClientException)
logger.debug("Client request rejected: {}", t.getMessage());
else {
logger.error("Request could not be processed: {}", t.getMessage());
logger.debug("Processing of request aborted by extension.", t);
}
logger.debug("Client request rejected: {}", t.getMessage(), t);
else
logger.error("Request could not be processed: {}", t.getMessage(), t);
return Response
.status(((GuacamoleException) t).getHttpStatusCode())
@@ -100,13 +98,11 @@ public class RESTExceptionMapper implements ExceptionMapper<Throwable> {
// Wrap unchecked exceptions
String message = t.getMessage();
if (message != null)
logger.error("Unexpected internal error: {}", message);
logger.error("Unexpected internal error: {}", message, t);
else
logger.error("An internal error occurred, but did not contain "
+ "an error message. Enable debug-level logging for "
+ "details.");
logger.debug("Unexpected error in REST endpoint.", t);
+ "details.", t);
return Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
@@ -433,12 +433,12 @@ public class AuthenticationService {
// This line SHOULD be unreachable unless a bug causes
// rethrowCause() to not actually rethrow the underlying failure
Throwable cause = e.getCause();
if (cause != null) {
logger.warn("An underlying internal error was not correctly rethrown by rethrowCause(): {}", cause.getMessage());
logger.debug("Internal error not rethrown by rethrowCause().", cause);
}
if (cause != null)
logger.warn("An underlying internal error was not correctly "
+ "rethrown by rethrowCause(): {}", cause.getMessage(), cause);
else
logger.warn("An underlying internal error was not correctly rethrown by rethrowCause().");
logger.warn("An underlying internal error was not correctly "
+ "rethrown by rethrowCause().");
throw e.getCauseAsGuacamoleException();
@@ -81,8 +81,7 @@ public class HashTokenSessionMap implements TokenSessionMap {
sessionTimeoutValue = environment.getProperty(API_SESSION_TIMEOUT, 60);
}
catch (GuacamoleException e) {
logger.error("Unable to read guacamole.properties: {}", e.getMessage());
logger.debug("Error while reading session timeout value.", e);
logger.error("Unable to read guacamole.properties: {}", e.getMessage(), e);
sessionTimeoutValue = 60;
}
@@ -69,8 +69,7 @@ public class InterceptedStreamMap<T extends Closeable> {
stream.close();
}
catch (IOException e) {
logger.warn("Unable to close intercepted stream: {}", e.getMessage());
logger.debug("I/O error prevented closure of intercepted stream.", e);
logger.warn("Unable to close intercepted stream: {}", e.getMessage(), e);
}
// Notify waiting threads that the stream has ended
@@ -130,8 +130,7 @@ public class OutputStreamInterceptingFilter
blob = BaseEncoding.base64().decode(data);
}
catch (IllegalArgumentException e) {
logger.warn("Received base64 data for intercepted stream was invalid.");
logger.debug("Decoding base64 data for intercepted stream failed.", e);
logger.warn("Received base64 data for intercepted stream was invalid.", e);
return null;
}
@@ -240,8 +240,7 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
tunnel = doConnect(tunnelRequest);
}
catch (GuacamoleException e) {
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage());
logger.debug("Error connecting WebSocket tunnel.", e);
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage(), e);
closeConnection(connection, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
return;
@@ -295,8 +294,7 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
// to pass within the WebSocket connection, logging
// each error appropriately.
catch (GuacamoleClientException e) {
logger.info("WebSocket connection terminated: {}", e.getMessage());
logger.debug("WebSocket connection terminated due to client error.", e);
logger.info("WebSocket connection terminated: {}", e.getMessage(), e);
closeConnection(connection, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
}
@@ -305,8 +303,7 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
closeConnection(connection, GuacamoleStatus.SUCCESS);
}
catch (GuacamoleException e) {
logger.error("Connection to guacd terminated abnormally: {}", e.getMessage());
logger.debug("Internal error during connection to guacd.", e);
logger.error("Connection to guacd terminated abnormally: {}", e.getMessage(), e);
closeConnection(connection, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
}
@@ -196,8 +196,7 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe
}
catch (GuacamoleException e) {
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage());
logger.debug("Error connecting WebSocket tunnel.", e);
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage(), e);
closeConnection(session, e.getStatus().getGuacamoleStatusCode(), e.getWebSocketCode());
return;
}
@@ -245,8 +244,7 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe
// to pass within the WebSocket connection, logging
// each error appropriately.
catch (GuacamoleClientException e) {
logger.info("WebSocket connection terminated: {}", e.getMessage());
logger.debug("WebSocket connection terminated due to client error.", e);
logger.info("WebSocket connection terminated: {}", e.getMessage(), e);
closeConnection(session, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
}
@@ -255,8 +253,7 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe
closeConnection(session, GuacamoleStatus.SUCCESS);
}
catch (GuacamoleException e) {
logger.error("Connection to guacd terminated abnormally: {}", e.getMessage());
logger.debug("Internal error during connection to guacd.", e);
logger.error("Connection to guacd terminated abnormally: {}", e.getMessage(), e);
closeConnection(session, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
}
@@ -268,8 +268,7 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
tunnel = doConnect(tunnelRequest);
}
catch (GuacamoleException e) {
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage());
logger.debug("Error connecting WebSocket tunnel.", e);
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage(), e);
closeConnection(outbound, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
return;
@@ -323,8 +322,7 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
// to pass within the WebSocket connection, logging
// each error appropriately.
catch (GuacamoleClientException e) {
logger.info("WebSocket connection terminated: {}", e.getMessage());
logger.debug("WebSocket connection terminated due to client error.", e);
logger.info("WebSocket connection terminated: {}", e.getMessage(), e);
closeConnection(outbound, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
}
@@ -333,8 +331,7 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
closeConnection(outbound, GuacamoleStatus.SUCCESS);
}
catch (GuacamoleException e) {
logger.error("Connection to guacd terminated abnormally: {}", e.getMessage());
logger.debug("Internal error during connection to guacd.", e);
logger.error("Connection to guacd terminated abnormally: {}", e.getMessage(), e);
closeConnection(outbound, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
}
@@ -0,0 +1 @@
org.apache.guacamole.GuacamoleServletContainerInitializer
@@ -0,0 +1 @@
org.apache.guacamole.log.GuacamoleLogbackServiceProvider
@@ -28,6 +28,14 @@
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<!-- Ensure that our own ServletContainerInitializer is used to initialize
logging, and that Logback does not potentially initialize SLF4J first (which
would prevent our custom logging from taking effect) -->
<context-param>
<param-name>logbackDisableServletContainerInitializer</param-name>
<param-value>true</param-value>
</context-param>
<!-- Request that the browser always revalidate its cached copy of index.html -->
<filter>
<filter-name>cacheRevalidationFilter</filter-name>