mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 21:27:40 +00:00
GUAC-587: Track and load authentication providers only within ExtensionModule. Default to basic auth. Explicitly deprecate.
This commit is contained in:
@@ -85,7 +85,7 @@ public class BasicServletContextListener extends GuiceServletContextListener {
|
|||||||
new EnvironmentModule(environment),
|
new EnvironmentModule(environment),
|
||||||
new LogModule(environment),
|
new LogModule(environment),
|
||||||
new ExtensionModule(environment),
|
new ExtensionModule(environment),
|
||||||
new RESTAuthModule(environment, sessionMap),
|
new RESTAuthModule(sessionMap),
|
||||||
new RESTServletModule(),
|
new RESTServletModule(),
|
||||||
new TunnelModule()
|
new TunnelModule()
|
||||||
);
|
);
|
||||||
|
@@ -39,10 +39,13 @@ import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A ClassLoader implementation which finds classes within a configurable
|
* A ClassLoader implementation which finds classes within a configurable
|
||||||
* directory. This directory is set within guacamole.properties.
|
* directory. This directory is set within guacamole.properties. This class
|
||||||
|
* is deprecated in favor of DirectoryClassLoader, which is automatically
|
||||||
|
* configured based on the presence/absence of GUACAMOLE_HOME/lib.
|
||||||
*
|
*
|
||||||
* @author Michael Jumper
|
* @author Michael Jumper
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public class GuacamoleClassLoader extends ClassLoader {
|
public class GuacamoleClassLoader extends ClassLoader {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -28,9 +28,11 @@ import java.io.File;
|
|||||||
import java.io.FileFilter;
|
import java.io.FileFilter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import net.sourceforge.guacamole.net.basic.BasicFileAuthenticationProvider;
|
||||||
import org.glyptodon.guacamole.GuacamoleException;
|
import org.glyptodon.guacamole.GuacamoleException;
|
||||||
import org.glyptodon.guacamole.environment.Environment;
|
import org.glyptodon.guacamole.environment.Environment;
|
||||||
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
|
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
|
||||||
|
import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
|
||||||
import org.glyptodon.guacamole.net.basic.resource.Resource;
|
import org.glyptodon.guacamole.net.basic.resource.Resource;
|
||||||
import org.glyptodon.guacamole.net.basic.resource.ResourceServlet;
|
import org.glyptodon.guacamole.net.basic.resource.ResourceServlet;
|
||||||
import org.glyptodon.guacamole.net.basic.resource.SequenceResource;
|
import org.glyptodon.guacamole.net.basic.resource.SequenceResource;
|
||||||
@@ -74,6 +76,12 @@ public class ExtensionModule extends ServletModule {
|
|||||||
*/
|
*/
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently-bound authentication provider, if any. At the moment, we
|
||||||
|
* only support one authentication provider loaded at any one time.
|
||||||
|
*/
|
||||||
|
private Class<? extends AuthenticationProvider> boundAuthenticationProvider = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the classloader that should be used as the parent classloader
|
* Returns the classloader that should be used as the parent classloader
|
||||||
* for all extensions. If the GUACAMOLE_HOME/lib directory exists, this
|
* for all extensions. If the GUACAMOLE_HOME/lib directory exists, this
|
||||||
@@ -113,9 +121,81 @@ public class ExtensionModule extends ServletModule {
|
|||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the value of the now-deprecated "auth-provider" property from
|
||||||
|
* guacamole.properties, returning the corresponding AuthenticationProvider
|
||||||
|
* class. If no authentication provider could be read, or the property is
|
||||||
|
* not present, null is returned.
|
||||||
|
*
|
||||||
|
* As this property is deprecated, this function will also log warning
|
||||||
|
* messages if the property is actually specified.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The value of the deprecated "auth-provider" property, or null if the
|
||||||
|
* property is not present.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation") // We must continue to use this property until it is truly no longer supported
|
||||||
|
private Class<AuthenticationProvider> getAuthProviderProperty() {
|
||||||
|
|
||||||
|
// Get and bind auth provider instance, if defined via property
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Use "auth-provider" property if present, but warn about deprecation
|
||||||
|
Class<AuthenticationProvider> authenticationProvider = environment.getProperty(BasicGuacamoleProperties.AUTH_PROVIDER);
|
||||||
|
if (authenticationProvider != null)
|
||||||
|
logger.warn("The \"auth-provider\" and \"lib-directory\" properties are now deprecated. Please use the \"extensions\" and \"lib\" directories within GUACAMOLE_HOME instead.");
|
||||||
|
|
||||||
|
return authenticationProvider;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
logger.warn("Value of deprecated \"auth-provider\" property within guacamole.properties is not valid: {}", e.getMessage());
|
||||||
|
logger.debug("Error reading authentication provider from guacamole.properties.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the given AuthenticationProvider class such that any service
|
||||||
|
* requiring access to the AuthenticationProvider can obtain it via
|
||||||
|
* injection.
|
||||||
|
*
|
||||||
|
* @param authenticationProvider
|
||||||
|
* The AuthenticationProvider class to bind.
|
||||||
|
*/
|
||||||
|
private void bindAuthenticationProvider(Class<? extends AuthenticationProvider> authenticationProvider) {
|
||||||
|
|
||||||
|
// Choose auth provider for binding if not already chosen
|
||||||
|
if (boundAuthenticationProvider != null)
|
||||||
|
boundAuthenticationProvider = authenticationProvider;
|
||||||
|
|
||||||
|
// If an auth provider is already chosen, skip and warn
|
||||||
|
else {
|
||||||
|
logger.debug("Ignoring AuthenticationProvider \"{}\".", authenticationProvider);
|
||||||
|
logger.warn("Only one authentication extension may be used at a time. Please "
|
||||||
|
+ "make sure that only one authentication extension is present "
|
||||||
|
+ "within the GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " "
|
||||||
|
+ "directory, and that you are not also specifying the deprecated "
|
||||||
|
+ "\"auth-provider\" property within guacamole.properties.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind authentication provider
|
||||||
|
logger.debug("Binding AuthenticationProvider \"{}\".", authenticationProvider);
|
||||||
|
bind(AuthenticationProvider.class).to(authenticationProvider).in(Singleton.class);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configureServlets() {
|
protected void configureServlets() {
|
||||||
|
|
||||||
|
// Load authentication provider from guacamole.properties for sake of backwards compatibility
|
||||||
|
Class<AuthenticationProvider> authProviderProperty = getAuthProviderProperty();
|
||||||
|
if (authProviderProperty != null)
|
||||||
|
bindAuthenticationProvider(authProviderProperty);
|
||||||
|
|
||||||
// Retrieve and validate extensions directory
|
// Retrieve and validate extensions directory
|
||||||
File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
|
File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
|
||||||
if (!extensionsDir.isDirectory())
|
if (!extensionsDir.isDirectory())
|
||||||
@@ -153,12 +233,10 @@ public class ExtensionModule extends ServletModule {
|
|||||||
javaScriptResources.addAll(extension.getJavaScriptResources());
|
javaScriptResources.addAll(extension.getJavaScriptResources());
|
||||||
cssResources.addAll(extension.getCSSResources());
|
cssResources.addAll(extension.getCSSResources());
|
||||||
|
|
||||||
// Load all authentication providers as singletons
|
// Attempt to load all authentication providers
|
||||||
Collection<Class<AuthenticationProvider>> authenticationProviders = extension.getAuthenticationProviderClasses();
|
Collection<Class<AuthenticationProvider>> authenticationProviders = extension.getAuthenticationProviderClasses();
|
||||||
for (Class<AuthenticationProvider> authenticationProvider : authenticationProviders) {
|
for (Class<AuthenticationProvider> authenticationProvider : authenticationProviders)
|
||||||
logger.debug("Binding AuthenticationProvider \"{}\".", authenticationProvider);
|
bindAuthenticationProvider(authenticationProvider);
|
||||||
bind(AuthenticationProvider.class).to(authenticationProvider).in(Singleton.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log successful loading of extension by name
|
// Log successful loading of extension by name
|
||||||
logger.info("Extension \"{}\" loaded.", extension.getName());
|
logger.info("Extension \"{}\" loaded.", extension.getName());
|
||||||
@@ -171,6 +249,12 @@ public class ExtensionModule extends ServletModule {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default to basic auth if nothing else chosen/provided
|
||||||
|
if (boundAuthenticationProvider == null) {
|
||||||
|
logger.info("Using default, \"basic\", XML-driven authentication.");
|
||||||
|
bindAuthenticationProvider(BasicFileAuthenticationProvider.class);
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamically generate app.js and app.css from extensions
|
// Dynamically generate app.js and app.css from extensions
|
||||||
serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources)));
|
serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources)));
|
||||||
serve("/app.css").with(new ResourceServlet(new SequenceResource(cssResources)));
|
serve("/app.css").with(new ResourceServlet(new SequenceResource(cssResources)));
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2013 Glyptodon LLC
|
* Copyright (C) 2015 Glyptodon LLC
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
package org.glyptodon.guacamole.net.basic.properties;
|
package org.glyptodon.guacamole.net.basic.properties;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import org.glyptodon.guacamole.GuacamoleException;
|
import org.glyptodon.guacamole.GuacamoleException;
|
||||||
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
|
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
|
||||||
import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader;
|
import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader;
|
||||||
@@ -30,14 +29,18 @@ import org.glyptodon.guacamole.properties.GuacamoleProperty;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A GuacamoleProperty whose value is the name of a class to use to
|
* A GuacamoleProperty whose value is the name of a class to use to
|
||||||
* authenticate users. This class must implement AuthenticationProvider.
|
* authenticate users. This class must implement AuthenticationProvider. Use
|
||||||
|
* of this property type is deprecated in favor of the
|
||||||
|
* GUACAMOLE_HOME/extensions directory.
|
||||||
*
|
*
|
||||||
* @author Michael Jumper
|
* @author Michael Jumper
|
||||||
*/
|
*/
|
||||||
public abstract class AuthenticationProviderProperty implements GuacamoleProperty<AuthenticationProvider> {
|
@Deprecated
|
||||||
|
public abstract class AuthenticationProviderProperty implements GuacamoleProperty<Class<AuthenticationProvider>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationProvider parseValue(String authProviderClassName) throws GuacamoleException {
|
@SuppressWarnings("unchecked") // Explicitly checked within by isAssignableFrom()
|
||||||
|
public Class<AuthenticationProvider> parseValue(String authProviderClassName) throws GuacamoleException {
|
||||||
|
|
||||||
// If no property provided, return null.
|
// If no property provided, return null.
|
||||||
if (authProviderClassName == null)
|
if (authProviderClassName == null)
|
||||||
@@ -46,35 +49,21 @@ public abstract class AuthenticationProviderProperty implements GuacamolePropert
|
|||||||
// Get auth provider instance
|
// Get auth provider instance
|
||||||
try {
|
try {
|
||||||
|
|
||||||
Object obj = GuacamoleClassLoader.getInstance().loadClass(authProviderClassName)
|
// Get authentication provider class
|
||||||
.getConstructor().newInstance();
|
Class<?> authProviderClass = GuacamoleClassLoader.getInstance().loadClass(authProviderClassName);
|
||||||
|
|
||||||
if (!(obj instanceof AuthenticationProvider))
|
// Verify the located class is actually a subclass of AuthenticationProvider
|
||||||
|
if (!AuthenticationProvider.class.isAssignableFrom(authProviderClass))
|
||||||
throw new GuacamoleException("Specified authentication provider class is not a AuthenticationProvider.");
|
throw new GuacamoleException("Specified authentication provider class is not a AuthenticationProvider.");
|
||||||
|
|
||||||
return (AuthenticationProvider) obj;
|
// Return located class
|
||||||
|
return (Class<AuthenticationProvider>) authProviderClass;
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (ClassNotFoundException e) {
|
catch (ClassNotFoundException e) {
|
||||||
throw new GuacamoleException("Authentication provider class not found", e);
|
throw new GuacamoleException("Authentication provider class not found", e);
|
||||||
}
|
}
|
||||||
catch (NoSuchMethodException e) {
|
|
||||||
throw new GuacamoleException("Default constructor for authentication provider not present", e);
|
|
||||||
}
|
|
||||||
catch (SecurityException e) {
|
|
||||||
throw new GuacamoleException("Creation of authentication provider disallowed; check your security settings", e);
|
|
||||||
}
|
|
||||||
catch (InstantiationException e) {
|
|
||||||
throw new GuacamoleException("Unable to instantiate authentication provider", e);
|
|
||||||
}
|
|
||||||
catch (IllegalAccessException e) {
|
|
||||||
throw new GuacamoleException("Unable to access default constructor of authentication provider", e);
|
|
||||||
}
|
|
||||||
catch (InvocationTargetException e) {
|
|
||||||
throw new GuacamoleException("Internal error in constructor of authentication provider", e.getTargetException());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
package org.glyptodon.guacamole.net.basic.properties;
|
package org.glyptodon.guacamole.net.basic.properties;
|
||||||
|
|
||||||
import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty;
|
|
||||||
import org.glyptodon.guacamole.properties.FileGuacamoleProperty;
|
import org.glyptodon.guacamole.properties.FileGuacamoleProperty;
|
||||||
import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty;
|
import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty;
|
||||||
|
|
||||||
@@ -40,8 +39,10 @@ public class BasicGuacamoleProperties {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The authentication provider to user when retrieving the authorized
|
* The authentication provider to user when retrieving the authorized
|
||||||
* configurations of a user.
|
* configurations of a user. This property is currently supported, but
|
||||||
|
* deprecated in favor of the GUACAMOLE_HOME/extensions directory.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public static final AuthenticationProviderProperty AUTH_PROVIDER = new AuthenticationProviderProperty() {
|
public static final AuthenticationProviderProperty AUTH_PROVIDER = new AuthenticationProviderProperty() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -50,8 +51,11 @@ public class BasicGuacamoleProperties {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The directory to search for authentication provider classes.
|
* The directory to search for authentication provider classes. This
|
||||||
|
* property is currently supported, but deprecated in favor of the
|
||||||
|
* GUACAMOLE_HOME/lib directory.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public static final FileGuacamoleProperty LIB_DIRECTORY = new FileGuacamoleProperty() {
|
public static final FileGuacamoleProperty LIB_DIRECTORY = new FileGuacamoleProperty() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -60,7 +64,9 @@ public class BasicGuacamoleProperties {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The comma-separated list of all classes to use as event listeners.
|
* The comma-separated list of all classes to use as event listeners. This
|
||||||
|
* property is currently supported, but deprecated in favor of declared
|
||||||
|
* event listeners within extension manifests.
|
||||||
*/
|
*/
|
||||||
public static final EventListenersProperty EVENT_LISTENERS = new EventListenersProperty() {
|
public static final EventListenersProperty EVENT_LISTENERS = new EventListenersProperty() {
|
||||||
|
|
||||||
|
@@ -30,10 +30,13 @@ import org.glyptodon.guacamole.properties.GuacamoleProperty;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A GuacamoleProperty whose value is a comma-separated list of class names,
|
* A GuacamoleProperty whose value is a comma-separated list of class names,
|
||||||
* where each class will be used as a listener for events.
|
* where each class will be used as a listener for events. This type of
|
||||||
|
* property is deprecated in favor of declaring event listeners within
|
||||||
|
* extension manifests.
|
||||||
*
|
*
|
||||||
* @author Michael Jumper
|
* @author Michael Jumper
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public abstract class EventListenersProperty implements GuacamoleProperty<Collection<Class>> {
|
public abstract class EventListenersProperty implements GuacamoleProperty<Collection<Class>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -23,10 +23,6 @@
|
|||||||
package org.glyptodon.guacamole.net.basic.rest;
|
package org.glyptodon.guacamole.net.basic.rest;
|
||||||
|
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
import org.glyptodon.guacamole.GuacamoleException;
|
|
||||||
import org.glyptodon.guacamole.environment.Environment;
|
|
||||||
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
|
|
||||||
import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
|
|
||||||
import org.glyptodon.guacamole.net.basic.rest.auth.AuthTokenGenerator;
|
import org.glyptodon.guacamole.net.basic.rest.auth.AuthTokenGenerator;
|
||||||
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
|
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
|
||||||
import org.glyptodon.guacamole.net.basic.rest.auth.SecureRandomAuthTokenGenerator;
|
import org.glyptodon.guacamole.net.basic.rest.auth.SecureRandomAuthTokenGenerator;
|
||||||
@@ -47,11 +43,6 @@ public class RESTAuthModule extends AbstractModule {
|
|||||||
*/
|
*/
|
||||||
private final Logger logger = LoggerFactory.getLogger(RESTAuthModule.class);
|
private final Logger logger = LoggerFactory.getLogger(RESTAuthModule.class);
|
||||||
|
|
||||||
/**
|
|
||||||
* The Guacamole server environment.
|
|
||||||
*/
|
|
||||||
private final Environment environment;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton instance of TokenSessionMap.
|
* Singleton instance of TokenSessionMap.
|
||||||
*/
|
*/
|
||||||
@@ -61,16 +52,11 @@ public class RESTAuthModule extends AbstractModule {
|
|||||||
* Creates a module which handles binding of authentication-related
|
* Creates a module which handles binding of authentication-related
|
||||||
* objects, including the singleton TokenSessionMap.
|
* objects, including the singleton TokenSessionMap.
|
||||||
*
|
*
|
||||||
* @param environment
|
|
||||||
* The environment to use when configuring authentication.
|
|
||||||
*
|
|
||||||
* @param tokenSessionMap
|
* @param tokenSessionMap
|
||||||
* An instance of TokenSessionMap to inject as a singleton wherever
|
* An instance of TokenSessionMap to inject as a singleton wherever
|
||||||
* needed.
|
* needed.
|
||||||
*/
|
*/
|
||||||
public RESTAuthModule(Environment environment,
|
public RESTAuthModule(TokenSessionMap tokenSessionMap) {
|
||||||
TokenSessionMap tokenSessionMap) {
|
|
||||||
this.environment = environment;
|
|
||||||
this.tokenSessionMap = tokenSessionMap;
|
this.tokenSessionMap = tokenSessionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,22 +70,6 @@ public class RESTAuthModule extends AbstractModule {
|
|||||||
bind(AuthenticationService.class);
|
bind(AuthenticationService.class);
|
||||||
bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class);
|
bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class);
|
||||||
|
|
||||||
// Get and bind auth provider instance, if defined via property
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Use "auth-provider" property if present, but warn about deprecation
|
|
||||||
AuthenticationProvider authProvider = environment.getProperty(BasicGuacamoleProperties.AUTH_PROVIDER);
|
|
||||||
if (authProvider != null) {
|
|
||||||
logger.warn("The \"auth-provider\" and \"lib-directory\" properties are now deprecated. Please use the \"extensions\" and \"lib\" directories within GUACAMOLE_HOME instead.");
|
|
||||||
bind(AuthenticationProvider.class).toInstance(authProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (GuacamoleException e) {
|
|
||||||
logger.warn("Value of deprecated \"auth-provider\" property within guacamole.properties is not valid: {}", e.getMessage());
|
|
||||||
logger.debug("Error reading authentication provider from guacamole.properties.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user