GUACAMOLE-1: Refactor org.glyptodon package/groupId to org.apache.

This commit is contained in:
Michael Jumper committed 2016-03-28 20:43:49 -07:00
1 parent 2358d88683
commit 6990344697
402 files changed
+1536 -1536

No files matched your search

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.servlet.GuacamoleHTTPTunnelServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Connects users to a tunnel associated with the authorized connection
* having the given ID.
*
* @author Michael Jumper
*/
@Singleton
public class BasicGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet {
/**
* Service for handling tunnel requests.
*/
@Inject
private TunnelRequestService tunnelRequestService;
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(BasicGuacamoleTunnelServlet.class);
@Override
protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException {
// Attempt to create HTTP tunnel
GuacamoleTunnel tunnel = tunnelRequestService.createTunnel(new HTTPTunnelRequest(request));
// If successful, warn of lack of WebSocket
logger.info("Using HTTP tunnel (not WebSocket). Performance may be sub-optimal.");
return tunnel;
}
}
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Stage;
import com.google.inject.servlet.GuiceServletContextListener;
import javax.servlet.ServletContextEvent;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.basic.extension.ExtensionModule;
import org.apache.guacamole.net.basic.log.LogModule;
import org.apache.guacamole.net.basic.rest.RESTServiceModule;
import org.apache.guacamole.net.basic.rest.auth.BasicTokenSessionMap;
import org.apache.guacamole.net.basic.rest.auth.TokenSessionMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A ServletContextListener to listen for initialization of the servlet context
* in order to set up dependency injection.
*
* @author James Muehlner
*/
public class BasicServletContextListener extends GuiceServletContextListener {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(BasicServletContextListener.class);
/**
* The Guacamole server environment.
*/
private Environment environment;
/**
* Singleton instance of a TokenSessionMap.
*/
private TokenSessionMap sessionMap;
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
try {
environment = new LocalEnvironment();
sessionMap = new BasicTokenSessionMap(environment);
}
catch (GuacamoleException e) {
logger.error("Unable to read guacamole.properties: {}", e.getMessage());
logger.debug("Error reading guacamole.properties.", e);
throw new RuntimeException(e);
}
super.contextInitialized(servletContextEvent);
}
@Override
protected Injector getInjector() {
return Guice.createInjector(Stage.PRODUCTION,
new EnvironmentModule(environment),
new LogModule(environment),
new ExtensionModule(environment),
new RESTServiceModule(sessionMap),
new TunnelModule()
);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
super.contextDestroyed(servletContextEvent);
// Shutdown TokenSessionMap
if (sessionMap != null)
sessionMap.shutdown();
}
}
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2014 Glyptodon LLC.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
/**
* Provides central storage for a cross-connection clipboard state. This
* clipboard state is shared only for a single HTTP session. Multiple HTTP
* sessions will all have their own state.
*
* @author Michael Jumper
*/
public class ClipboardState {
/**
* The maximum number of bytes to track.
*/
private static final int MAXIMUM_LENGTH = 262144;
/**
* The mimetype of the current contents.
*/
private String mimetype = "text/plain";
/**
* The mimetype of the pending contents.
*/
private String pending_mimetype = "text/plain";
/**
* The current contents.
*/
private byte[] contents = new byte[0];
/**
* The pending clipboard contents.
*/
private final byte[] pending = new byte[MAXIMUM_LENGTH];
/**
* The length of the pending data, in bytes.
*/
private int pending_length = 0;
/**
* The timestamp of the last contents update.
*/
private long last_update = 0;
/**
* Returns the current clipboard contents.
* @return The current clipboard contents
*/
public synchronized byte[] getContents() {
return contents;
}
/**
* Returns the mimetype of the current clipboard contents.
* @return The mimetype of the current clipboard contents.
*/
public synchronized String getMimetype() {
return mimetype;
}
/**
* Begins a new update of the clipboard contents. The actual contents will
* not be saved until commit() is called.
*
* @param mimetype The mimetype of the contents being added.
*/
public synchronized void begin(String mimetype) {
pending_length = 0;
this.pending_mimetype = mimetype;
}
/**
* Appends the given data to the clipboard contents.
*
* @param data The raw data to append.
*/
public synchronized void append(byte[] data) {
// Calculate size of copy
int length = data.length;
int remaining = pending.length - pending_length;
if (remaining < length)
length = remaining;
// Append data
System.arraycopy(data, 0, pending, pending_length, length);
pending_length += length;
}
/**
* Commits the pending contents to the clipboard, notifying any threads
* waiting for clipboard updates.
*/
public synchronized void commit() {
// Commit contents
mimetype = pending_mimetype;
contents = new byte[pending_length];
System.arraycopy(pending, 0, contents, 0, pending_length);
// Notify of update
last_update = System.currentTimeMillis();
this.notifyAll();
}
/**
* Wait up to the given timeout for new clipboard data.
*
* @param timeout The amount of time to wait, in milliseconds.
* @return true if the contents were updated within the timeframe given,
* false otherwise.
*/
public synchronized boolean waitForContents(int timeout) {
// Wait for new contents if it's been a while
if (System.currentTimeMillis() - last_update > timeout) {
try {
this.wait(timeout);
return true;
}
catch (InterruptedException e) { /* ignore */ }
}
return false;
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import com.google.inject.AbstractModule;
import org.apache.guacamole.environment.Environment;
/**
* Guice module which binds the base Guacamole server environment.
*
* @author Michael Jumper
*/
public class EnvironmentModule extends AbstractModule {
/**
* The Guacamole server environment.
*/
private final Environment environment;
/**
* Creates a new EnvironmentModule which will bind the given environment
* for future injection.
*
* @param environment
* The environment to bind.
*/
public EnvironmentModule(Environment environment) {
this.environment = environment;
}
@Override
protected void configure() {
// Bind environment
bind(Environment.class).toInstance(environment);
}
}
@@ -0,0 +1,185 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import java.io.File;
import java.io.FilenameFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.basic.properties.BasicGuacamoleProperties;
/**
* A ClassLoader implementation which finds classes within a configurable
* 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
*/
@Deprecated
public class GuacamoleClassLoader extends ClassLoader {
/**
* Class loader which will load classes from the classpath specified
* in guacamole.properties.
*/
private URLClassLoader classLoader = null;
/**
* Any exception that occurs while the class loader is being instantiated.
*/
private static GuacamoleException exception = null;
/**
* Singleton instance of the GuacamoleClassLoader.
*/
private static GuacamoleClassLoader instance = null;
static {
try {
// Attempt to create singleton classloader which loads classes from
// all .jar's in the lib directory defined in guacamole.properties
instance = AccessController.doPrivileged(new PrivilegedExceptionAction<GuacamoleClassLoader>() {
@Override
public GuacamoleClassLoader run() throws GuacamoleException {
// TODONT: This should be injected, but GuacamoleClassLoader will be removed soon.
Environment environment = new LocalEnvironment();
return new GuacamoleClassLoader(
environment.getProperty(BasicGuacamoleProperties.LIB_DIRECTORY)
);
}
});
}
catch (PrivilegedActionException e) {
// On error, record exception
exception = (GuacamoleException) e.getException();
}
}
/**
* Creates a new GuacamoleClassLoader which reads classes from the given
* directory.
*
* @param libDirectory The directory to load classes from.
* @throws GuacamoleException If the file given is not a director, or if
* an error occurs while constructing the URL
* for the backing classloader.
*/
private GuacamoleClassLoader(File libDirectory) throws GuacamoleException {
// If no directory provided, just direct requests to parent classloader
if (libDirectory == null)
return;
// Validate directory is indeed a directory
if (!libDirectory.isDirectory())
throw new GuacamoleException(libDirectory + " is not a directory.");
// Get list of URLs for all .jar's in the lib directory
Collection<URL> jarURLs = new ArrayList<URL>();
File[] files = libDirectory.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
// If it ends with .jar, accept the file
return name.endsWith(".jar");
}
});
// Verify directory was successfully read
if (files == null)
throw new GuacamoleException("Unable to read contents of directory " + libDirectory);
// Add the URL for each .jar to the jar URL list
for (File file : files) {
try {
jarURLs.add(file.toURI().toURL());
}
catch (MalformedURLException e) {
throw new GuacamoleException(e);
}
}
// Set delegate classloader to new URLClassLoader which loads from the
// .jars found above.
URL[] urls = new URL[jarURLs.size()];
classLoader = new URLClassLoader(
jarURLs.toArray(urls),
getClass().getClassLoader()
);
}
/**
* Returns an instance of a GuacamoleClassLoader which finds classes
* within the directory configured in guacamole.properties.
*
* @return An instance of a GuacamoleClassLoader.
* @throws GuacamoleException If no instance could be returned due to an
* error.
*/
public static GuacamoleClassLoader getInstance() throws GuacamoleException {
// If instance could not be created, rethrow original exception
if (exception != null) throw exception;
return instance;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// If no classloader, use default loader
if (classLoader == null)
return Class.forName(name);
// Otherwise, delegate
return classLoader.loadClass(name);
}
}
@@ -0,0 +1,222 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.UserContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Contains Guacamole-specific user information which is tied to the current
* session, such as the UserContext and current clipboard state.
*
* @author Michael Jumper
*/
public class GuacamoleSession {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(GuacamoleSession.class);
/**
* The user associated with this session.
*/
private AuthenticatedUser authenticatedUser;
/**
* All UserContexts associated with this session. Each
* AuthenticationProvider may provide its own UserContext.
*/
private List<UserContext> userContexts;
/**
* All currently-active tunnels, indexed by tunnel UUID.
*/
private final Map<String, GuacamoleTunnel> tunnels = new ConcurrentHashMap<String, GuacamoleTunnel>();
/**
* The last time this session was accessed.
*/
private long lastAccessedTime;
/**
* Creates a new Guacamole session associated with the given
* AuthenticatedUser and UserContexts.
*
* @param environment
* The environment of the Guacamole server associated with this new
* session.
*
* @param authenticatedUser
* The authenticated user to associate this session with.
*
* @param userContexts
* The List of UserContexts to associate with this session.
*
* @throws GuacamoleException
* If an error prevents the session from being created.
*/
public GuacamoleSession(Environment environment,
AuthenticatedUser authenticatedUser,
List<UserContext> userContexts)
throws GuacamoleException {
this.lastAccessedTime = System.currentTimeMillis();
this.authenticatedUser = authenticatedUser;
this.userContexts = userContexts;
}
/**
* Returns the authenticated user associated with this session.
*
* @return
* The authenticated user associated with this session.
*/
public AuthenticatedUser getAuthenticatedUser() {
return authenticatedUser;
}
/**
* Replaces the authenticated user associated with this session with the
* given authenticated user.
*
* @param authenticatedUser
* The authenticated user to associated with this session.
*/
public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) {
this.authenticatedUser = authenticatedUser;
}
/**
* Returns a list of all UserContexts associated with this session. Each
* AuthenticationProvider currently loaded by Guacamole may provide its own
* UserContext for any successfully-authenticated user.
*
* @return
* An unmodifiable list of all UserContexts associated with this
* session.
*/
public List<UserContext> getUserContexts() {
return Collections.unmodifiableList(userContexts);
}
/**
* Replaces all UserContexts associated with this session with the given
* List of UserContexts.
*
* @param userContexts
* The List of UserContexts to associate with this session.
*/
public void setUserContexts(List<UserContext> userContexts) {
this.userContexts = userContexts;
}
/**
* Returns whether this session has any associated active tunnels.
*
* @return true if this session has any associated active tunnels,
* false otherwise.
*/
public boolean hasTunnels() {
return !tunnels.isEmpty();
}
/**
* Returns a map of all active tunnels associated with this session, where
* each key is the String representation of the tunnel's UUID. Changes to
* this map immediately affect the set of tunnels associated with this
* session. A tunnel need not be present here to be used by the user
* associated with this session, but tunnels not in this set will not
* be taken into account when determining whether a session is in use.
*
* @return A map of all active tunnels associated with this session.
*/
public Map<String, GuacamoleTunnel> getTunnels() {
return tunnels;
}
/**
* Associates the given tunnel with this session, such that it is taken
* into account when determining session activity.
*
* @param tunnel The tunnel to associate with this session.
*/
public void addTunnel(GuacamoleTunnel tunnel) {
tunnels.put(tunnel.getUUID().toString(), tunnel);
}
/**
* Disassociates the tunnel having the given UUID from this session.
*
* @param uuid The UUID of the tunnel to disassociate from this session.
* @return true if the tunnel existed and was removed, false otherwise.
*/
public boolean removeTunnel(String uuid) {
return tunnels.remove(uuid) != null;
}
/**
* Updates this session, marking it as accessed.
*/
public void access() {
lastAccessedTime = System.currentTimeMillis();
}
/**
* Returns the time this session was last accessed, as the number of
* milliseconds since midnight January 1, 1970 GMT. Session access must
* be explicitly marked through calls to the access() function.
*
* @return The time this session was last accessed.
*/
public long getLastAccessedTime() {
return lastAccessedTime;
}
/**
* Closes all associated tunnels and prevents any further use of this
* session.
*/
public void invalidate() {
// Close all associated tunnels, if possible
for (GuacamoleTunnel tunnel : tunnels.values()) {
try {
tunnel.close();
}
catch (GuacamoleException e) {
logger.debug("Unable to close tunnel.", e);
}
}
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
/**
* HTTP-specific implementation of TunnelRequest.
*
* @author Michael Jumper
*/
public class HTTPTunnelRequest extends TunnelRequest {
/**
* A copy of the parameters obtained from the HttpServletRequest used to
* construct the HTTPTunnelRequest.
*/
private final Map<String, List<String>> parameterMap =
new HashMap<String, List<String>>();
/**
* Creates a HTTPTunnelRequest which copies and exposes the parameters
* from the given HttpServletRequest.
*
* @param request
* The HttpServletRequest to copy parameter values from.
*/
@SuppressWarnings("unchecked") // getParameterMap() is defined as returning Map<String, String[]>
public HTTPTunnelRequest(HttpServletRequest request) {
// For each parameter
for (Map.Entry<String, String[]> mapEntry : ((Map<String, String[]>)
request.getParameterMap()).entrySet()) {
// Get parameter name and corresponding values
String parameterName = mapEntry.getKey();
List<String> parameterValues = Arrays.asList(mapEntry.getValue());
// Store copy of all values in our own map
parameterMap.put(
parameterName,
new ArrayList<String>(parameterValues)
);
}
}
@Override
public String getParameter(String name) {
List<String> values = getParameterValues(name);
// Return the first value from the list if available
if (values != null && !values.isEmpty())
return values.get(0);
return null;
}
@Override
public List<String> getParameterValues(String name) {
return parameterMap.get(name);
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import com.google.inject.Module;
/**
* Generic means of loading a tunnel without adding explicit dependencies within
* the main ServletModule, as not all servlet containers may have the classes
* required by all tunnel implementations.
*
* @author Michael Jumper
*/
public interface TunnelLoader extends Module {
/**
* Checks whether this type of tunnel is supported by the servlet container.
*
* @return true if this type of tunnel is supported and can be loaded
* without errors, false otherwise.
*/
public boolean isSupported();
}
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import com.google.inject.servlet.ServletModule;
import java.lang.reflect.InvocationTargetException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Module which loads tunnel implementations.
*
* @author Michael Jumper
*/
public class TunnelModule extends ServletModule {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(TunnelModule.class);
/**
* Classnames of all implementation-specific WebSocket tunnel modules.
*/
private static final String[] WEBSOCKET_MODULES = {
"org.apache.guacamole.net.basic.websocket.WebSocketTunnelModule",
"org.apache.guacamole.net.basic.websocket.jetty8.WebSocketTunnelModule",
"org.apache.guacamole.net.basic.websocket.jetty9.WebSocketTunnelModule",
"org.apache.guacamole.net.basic.websocket.tomcat.WebSocketTunnelModule"
};
private boolean loadWebSocketModule(String classname) {
try {
// Attempt to find WebSocket module
Class<?> module = Class.forName(classname);
// Create loader
TunnelLoader loader = (TunnelLoader) module.getConstructor().newInstance();
// Install module, if supported
if (loader.isSupported()) {
install(loader);
return true;
}
}
// If no such class or constructor, etc., then this particular
// WebSocket support is not present
catch (ClassNotFoundException e) {}
catch (NoClassDefFoundError e) {}
catch (NoSuchMethodException e) {}
// Log errors which indicate bugs
catch (InstantiationException e) {
logger.debug("Error instantiating WebSocket module.", e);
}
catch (IllegalAccessException e) {
logger.debug("Error instantiating WebSocket module.", e);
}
catch (InvocationTargetException e) {
logger.debug("Error instantiating WebSocket module.", e);
}
// Load attempt failed
return false;
}
@Override
protected void configureServlets() {
bind(TunnelRequestService.class);
// Set up HTTP tunnel
serve("/tunnel").with(BasicGuacamoleTunnelServlet.class);
// Try to load each WebSocket tunnel in sequence
for (String classname : WEBSOCKET_MODULES) {
if (loadWebSocketModule(classname)) {
logger.debug("WebSocket module loaded: {}", classname);
return;
}
}
// Warn of lack of WebSocket
logger.info("WebSocket support NOT present. Only HTTP will be used.");
}
}
@@ -0,0 +1,372 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import java.util.List;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
/**
* A request object which provides only the functions absolutely required to
* retrieve and connect to a tunnel.
*
* @author Michael Jumper
*/
public abstract class TunnelRequest {
/**
* The name of the request parameter containing the user's authentication
* token.
*/
public static final String AUTH_TOKEN_PARAMETER = "token";
/**
* The name of the parameter containing the identifier of the
* AuthenticationProvider associated with the UserContext containing the
* object to which a tunnel is being requested.
*/
public static final String AUTH_PROVIDER_IDENTIFIER_PARAMETER = "GUAC_DATA_SOURCE";
/**
* The name of the parameter specifying the type of object to which a
* tunnel is being requested. Currently, this may be "c" for a Guacamole
* connection, or "g" for a Guacamole connection group.
*/
public static final String TYPE_PARAMETER = "GUAC_TYPE";
/**
* The name of the parameter containing the unique identifier of the object
* to which a tunnel is being requested.
*/
public static final String IDENTIFIER_PARAMETER = "GUAC_ID";
/**
* The name of the parameter containing the desired display width, in
* pixels.
*/
public static final String WIDTH_PARAMETER = "GUAC_WIDTH";
/**
* The name of the parameter containing the desired display height, in
* pixels.
*/
public static final String HEIGHT_PARAMETER = "GUAC_HEIGHT";
/**
* The name of the parameter containing the desired display resolution, in
* DPI.
*/
public static final String DPI_PARAMETER = "GUAC_DPI";
/**
* The name of the parameter specifying one supported audio mimetype. This
* will normally appear multiple times within a single tunnel request -
* once for each mimetype.
*/
public static final String AUDIO_PARAMETER = "GUAC_AUDIO";
/**
* The name of the parameter specifying one supported video mimetype. This
* will normally appear multiple times within a single tunnel request -
* once for each mimetype.
*/
public static final String VIDEO_PARAMETER = "GUAC_VIDEO";
/**
* The name of the parameter specifying one supported image mimetype. This
* will normally appear multiple times within a single tunnel request -
* once for each mimetype.
*/
public static final String IMAGE_PARAMETER = "GUAC_IMAGE";
/**
* All supported object types that can be used as the destination of a
* tunnel.
*/
public static enum Type {
/**
* A Guacamole connection.
*/
CONNECTION("c"),
/**
* A Guacamole connection group.
*/
CONNECTION_GROUP("g");
/**
* The parameter value which denotes a destination object of this type.
*/
final String PARAMETER_VALUE;
/**
* Defines a Type having the given corresponding parameter value.
*
* @param value
* The parameter value which denotes a destination object of this
* type.
*/
Type(String value) {
PARAMETER_VALUE = value;
}
};
/**
* Returns the value of the parameter having the given name.
*
* @param name
* The name of the parameter to return.
*
* @return
* The value of the parameter having the given name, or null if no such
* parameter was specified.
*/
public abstract String getParameter(String name);
/**
* Returns a list of all values specified for the given parameter.
*
* @param name
* The name of the parameter to return.
*
* @return
* All values of the parameter having the given name , or null if no
* such parameter was specified.
*/
public abstract List<String> getParameterValues(String name);
/**
* Returns the value of the parameter having the given name, throwing an
* exception if the parameter is missing.
*
* @param name
* The name of the parameter to return.
*
* @return
* The value of the parameter having the given name.
*
* @throws GuacamoleException
* If the parameter is not present in the request.
*/
public String getRequiredParameter(String name) throws GuacamoleException {
// Pull requested parameter, aborting if absent
String value = getParameter(name);
if (value == null)
throw new GuacamoleClientException("Parameter \"" + name + "\" is required.");
return value;
}
/**
* Returns the integer value of the parameter having the given name,
* throwing an exception if the parameter cannot be parsed.
*
* @param name
* The name of the parameter to return.
*
* @return
* The integer value of the parameter having the given name, or null if
* the parameter is missing.
*
* @throws GuacamoleException
* If the parameter is not a valid integer.
*/
public Integer getIntegerParameter(String name) throws GuacamoleException {
// Pull requested parameter
String value = getParameter(name);
if (value == null)
return null;
// Attempt to parse as an integer
try {
return Integer.parseInt(value);
}
// Rethrow any parsing error as a GuacamoleClientException
catch (NumberFormatException e) {
throw new GuacamoleClientException("Parameter \"" + name + "\" must be a valid integer.", e);
}
}
/**
* Returns the authentication token associated with this tunnel request.
*
* @return
* The authentication token associated with this tunnel request, or
* null if no authentication token is present.
*/
public String getAuthenticationToken() {
return getParameter(AUTH_TOKEN_PARAMETER);
}
/**
* Returns the identifier of the AuthenticationProvider associated with the
* UserContext from which the connection or connection group is to be
* retrieved when the tunnel is created. In the context of the REST API and
* the JavaScript side of the web application, this is referred to as the
* data source identifier.
*
* @return
* The identifier of the AuthenticationProvider associated with the
* UserContext from which the connection or connection group is to be
* retrieved when the tunnel is created.
*
* @throws GuacamoleException
* If the identifier was not present in the request.
*/
public String getAuthenticationProviderIdentifier()
throws GuacamoleException {
return getRequiredParameter(AUTH_PROVIDER_IDENTIFIER_PARAMETER);
}
/**
* Returns the type of object for which the tunnel is being requested.
*
* @return
* The type of object for which the tunnel is being requested.
*
* @throws GuacamoleException
* If the type was not present in the request, or if the type requested
* is in the wrong format.
*/
public Type getType() throws GuacamoleException {
String type = getRequiredParameter(TYPE_PARAMETER);
// For each possible object type
for (Type possibleType : Type.values()) {
// Match against defined parameter value
if (type.equals(possibleType.PARAMETER_VALUE))
return possibleType;
}
throw new GuacamoleClientException("Illegal identifier - unknown type.");
}
/**
* Returns the identifier of the destination of the tunnel being requested.
* As there are multiple types of destination objects available, and within
* multiple data sources, the associated object type and data source are
* also necessary to determine what this identifier refers to.
*
* @return
* The identifier of the destination of the tunnel being requested.
*
* @throws GuacamoleException
* If the identifier was not present in the request.
*/
public String getIdentifier() throws GuacamoleException {
return getRequiredParameter(IDENTIFIER_PARAMETER);
}
/**
* Returns the display width desired for the Guacamole session over the
* tunnel being requested.
*
* @return
* The display width desired for the Guacamole session over the tunnel
* being requested, or null if no width was given.
*
* @throws GuacamoleException
* If the width specified was not a valid integer.
*/
public Integer getWidth() throws GuacamoleException {
return getIntegerParameter(WIDTH_PARAMETER);
}
/**
* Returns the display height desired for the Guacamole session over the
* tunnel being requested.
*
* @return
* The display height desired for the Guacamole session over the tunnel
* being requested, or null if no width was given.
*
* @throws GuacamoleException
* If the height specified was not a valid integer.
*/
public Integer getHeight() throws GuacamoleException {
return getIntegerParameter(HEIGHT_PARAMETER);
}
/**
* Returns the display resolution desired for the Guacamole session over
* the tunnel being requested, in DPI.
*
* @return
* The display resolution desired for the Guacamole session over the
* tunnel being requested, or null if no resolution was given.
*
* @throws GuacamoleException
* If the resolution specified was not a valid integer.
*/
public Integer getDPI() throws GuacamoleException {
return getIntegerParameter(DPI_PARAMETER);
}
/**
* Returns a list of all audio mimetypes declared as supported within the
* tunnel request.
*
* @return
* A list of all audio mimetypes declared as supported within the
* tunnel request, or null if no mimetypes were specified.
*/
public List<String> getAudioMimetypes() {
return getParameterValues(AUDIO_PARAMETER);
}
/**
* Returns a list of all video mimetypes declared as supported within the
* tunnel request.
*
* @return
* A list of all video mimetypes declared as supported within the
* tunnel request, or null if no mimetypes were specified.
*/
public List<String> getVideoMimetypes() {
return getParameterValues(VIDEO_PARAMETER);
}
/**
* Returns a list of all image mimetypes declared as supported within the
* tunnel request.
*
* @return
* A list of all image mimetypes declared as supported within the
* tunnel request, or null if no mimetypes were specified.
*/
public List<String> getImageMimetypes() {
return getParameterValues(IMAGE_PARAMETER);
}
}
@@ -0,0 +1,359 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.List;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.net.DelegatingGuacamoleTunnel;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.basic.rest.ObjectRetrievalService;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.apache.guacamole.protocol.GuacamoleClientInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class that takes a standard request from the Guacamole JavaScript
* client and produces the corresponding GuacamoleTunnel. The implementation
* of this utility is specific to the form of request used by the upstream
* Guacamole web application, and is not necessarily useful to applications
* that use purely the Guacamole API.
*
* @author Michael Jumper
* @author Vasily Loginov
*/
@Singleton
public class TunnelRequestService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(TunnelRequestService.class);
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/**
* Reads and returns the client information provided within the given
* request.
*
* @param request
* The request describing tunnel to create.
*
* @return GuacamoleClientInformation
* An object containing information about the client sending the tunnel
* request.
*
* @throws GuacamoleException
* If the parameters of the tunnel request are invalid.
*/
protected GuacamoleClientInformation getClientInformation(TunnelRequest request)
throws GuacamoleException {
// Get client information
GuacamoleClientInformation info = new GuacamoleClientInformation();
// Set width if provided
Integer width = request.getWidth();
if (width != null)
info.setOptimalScreenWidth(width);
// Set height if provided
Integer height = request.getHeight();
if (height != null)
info.setOptimalScreenHeight(height);
// Set resolution if provided
Integer dpi = request.getDPI();
if (dpi != null)
info.setOptimalResolution(dpi);
// Add audio mimetypes
List<String> audioMimetypes = request.getAudioMimetypes();
if (audioMimetypes != null)
info.getAudioMimetypes().addAll(audioMimetypes);
// Add video mimetypes
List<String> videoMimetypes = request.getVideoMimetypes();
if (videoMimetypes != null)
info.getVideoMimetypes().addAll(videoMimetypes);
// Add image mimetypes
List<String> imageMimetypes = request.getImageMimetypes();
if (imageMimetypes != null)
info.getImageMimetypes().addAll(imageMimetypes);
return info;
}
/**
* Creates a new tunnel using which is connected to the connection or
* connection group identifier by the given ID. Client information
* is specified in the {@code info} parameter.
*
* @param context
* The UserContext associated with the user for whom the tunnel is
* being created.
*
* @param type
* The type of object being connected to (connection or group).
*
* @param id
* The id of the connection or group being connected to.
*
* @param info
* Information describing the connected Guacamole client.
*
* @return
* A new tunnel, connected as required by the request.
*
* @throws GuacamoleException
* If an error occurs while creating the tunnel.
*/
protected GuacamoleTunnel createConnectedTunnel(UserContext context,
final TunnelRequest.Type type, String id,
GuacamoleClientInformation info)
throws GuacamoleException {
// Create connected tunnel from identifier
GuacamoleTunnel tunnel = null;
switch (type) {
// Connection identifiers
case CONNECTION: {
// Get connection directory
Directory<Connection> directory = context.getConnectionDirectory();
// Get authorized connection
Connection connection = directory.get(id);
if (connection == null) {
logger.info("Connection \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier());
throw new GuacamoleSecurityException("Requested connection is not authorized.");
}
// Connect tunnel
tunnel = connection.connect(info);
logger.info("User \"{}\" connected to connection \"{}\".", context.self().getIdentifier(), id);
break;
}
// Connection group identifiers
case CONNECTION_GROUP: {
// Get connection group directory
Directory<ConnectionGroup> directory = context.getConnectionGroupDirectory();
// Get authorized connection group
ConnectionGroup group = directory.get(id);
if (group == null) {
logger.info("Connection group \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier());
throw new GuacamoleSecurityException("Requested connection group is not authorized.");
}
// Connect tunnel
tunnel = group.connect(info);
logger.info("User \"{}\" connected to group \"{}\".", context.self().getIdentifier(), id);
break;
}
// Type is guaranteed to be one of the above
default:
assert(false);
}
return tunnel;
}
/**
* Associates the given tunnel with the given session, returning a wrapped
* version of the same tunnel which automatically handles closure and
* removal from the session.
*
* @param tunnel
* The connected tunnel to wrap and monitor.
*
* @param authToken
* The authentication token associated with the given session. If
* provided, this token will be automatically invalidated (and the
* corresponding session destroyed) if tunnel errors imply that the
* user is no longer authorized.
*
* @param session
* The Guacamole session to associate the tunnel with.
*
* @param type
* The type of object being connected to (connection or group).
*
* @param id
* The id of the connection or group being connected to.
*
* @return
* A new tunnel, associated with the given session, which delegates all
* functionality to the given tunnel while monitoring and automatically
* handling closure.
*
* @throws GuacamoleException
* If an error occurs while obtaining the tunnel.
*/
protected GuacamoleTunnel createAssociatedTunnel(GuacamoleTunnel tunnel,
final String authToken, final GuacamoleSession session,
final TunnelRequest.Type type, final String id)
throws GuacamoleException {
// Monitor tunnel closure and data
GuacamoleTunnel monitoredTunnel = new DelegatingGuacamoleTunnel(tunnel) {
/**
* The time the connection began, measured in milliseconds since
* midnight, January 1, 1970 UTC.
*/
private final long connectionStartTime = System.currentTimeMillis();
@Override
public void close() throws GuacamoleException {
long connectionEndTime = System.currentTimeMillis();
long duration = connectionEndTime - connectionStartTime;
// Log closure
switch (type) {
// Connection identifiers
case CONNECTION:
logger.info("User \"{}\" disconnected from connection \"{}\". Duration: {} milliseconds",
session.getAuthenticatedUser().getIdentifier(), id, duration);
break;
// Connection group identifiers
case CONNECTION_GROUP:
logger.info("User \"{}\" disconnected from connection group \"{}\". Duration: {} milliseconds",
session.getAuthenticatedUser().getIdentifier(), id, duration);
break;
// Type is guaranteed to be one of the above
default:
assert(false);
}
try {
// Close and clean up tunnel
session.removeTunnel(getUUID().toString());
super.close();
}
// Ensure any associated session is invalidated if unauthorized
catch (GuacamoleUnauthorizedException e) {
// If there is an associated auth token, invalidate it
if (authenticationService.destroyGuacamoleSession(authToken))
logger.debug("Implicitly invalidated session for token \"{}\".", authToken);
// Continue with exception processing
throw e;
}
}
};
// Associate tunnel with session
session.addTunnel(monitoredTunnel);
return monitoredTunnel;
}
/**
* Creates a new tunnel using the parameters and credentials present in
* the given request.
*
* @param request
* The request describing the tunnel to create.
*
* @return
* The created tunnel, or null if the tunnel could not be created.
*
* @throws GuacamoleException
* If an error occurs while creating the tunnel.
*/
public GuacamoleTunnel createTunnel(TunnelRequest request)
throws GuacamoleException {
// Parse request parameters
String authToken = request.getAuthenticationToken();
String id = request.getIdentifier();
TunnelRequest.Type type = request.getType();
String authProviderIdentifier = request.getAuthenticationProviderIdentifier();
GuacamoleClientInformation info = getClientInformation(request);
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
try {
// Create connected tunnel using provided connection ID and client information
GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type, id, info);
// Associate tunnel with session
return createAssociatedTunnel(tunnel, authToken, session, type, id);
}
// Ensure any associated session is invalidated if unauthorized
catch (GuacamoleUnauthorizedException e) {
// If there is an associated auth token, invalidate it
if (authenticationService.destroyGuacamoleSession(authToken))
logger.debug("Implicitly invalidated session for token \"{}\".", authToken);
// Continue with exception processing
throw e;
}
}
}
@@ -0,0 +1,255 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.auth;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.TreeMap;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
/**
* Mapping of username/password pair to configuration set. In addition to basic
* storage of the username, password, and configurations, this class also
* provides password validation functions.
*
* @author Mike Jumper
*/
public class Authorization {
/**
* All supported password encodings.
*/
public static enum Encoding {
/**
* Plain-text password (not hashed at all).
*/
PLAIN_TEXT,
/**
* Password hashed with MD5.
*/
MD5
}
/**
* The username being authorized.
*/
private String username;
/**
* The password corresponding to the username being authorized, which may
* be hashed.
*/
private String password;
/**
* The encoding used when the password was hashed.
*/
private Encoding encoding = Encoding.PLAIN_TEXT;
/**
* Map of all authorized configurations, indexed by configuration name.
*/
private Map<String, GuacamoleConfiguration> configs = new
TreeMap<String, GuacamoleConfiguration>();
/**
* Lookup table of hex bytes characters by value.
*/
private static final char HEX_CHARS[] = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
/**
* Produces a String containing the bytes provided in hexadecimal notation.
*
* @param bytes The bytes to convert into hex.
* @return A String containing the hex representation of the given bytes.
*/
private static String getHexString(byte[] bytes) {
// If null byte array given, return null
if (bytes == null)
return null;
// Create string builder for holding the hex representation,
// pre-calculating the exact length
StringBuilder hex = new StringBuilder(2 * bytes.length);
// Convert each byte into a pair of hex digits
for (byte b : bytes) {
hex.append(HEX_CHARS[(b & 0xF0) >> 4])
.append(HEX_CHARS[ b & 0x0F ]);
}
// Return the string produced
return hex.toString();
}
/**
* Returns the username associated with this authorization.
*
* @return The username associated with this authorization.
*/
public String getUsername() {
return username;
}
/**
* Sets the username associated with this authorization.
*
* @param username The username to associate with this authorization.
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Returns the password associated with this authorization, which may be
* encoded or hashed.
*
* @return The password associated with this authorization.
*/
public String getPassword() {
return password;
}
/**
* Sets the password associated with this authorization, which must be
* encoded using the encoding specified with setEncoding(). By default,
* passwords are plain text.
*
* @param password Sets the password associated with this authorization.
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Returns the encoding used to hash the password, if any.
*
* @return The encoding used to hash the password.
*/
public Encoding getEncoding() {
return encoding;
}
/**
* Sets the encoding which will be used to hash the password or when
* comparing a given password for validation.
*
* @param encoding The encoding to use for password hashing.
*/
public void setEncoding(Encoding encoding) {
this.encoding = encoding;
}
/**
* Returns whether a given username/password pair is authorized based on
* the stored username and password. The password given must be plain text.
* It will be hashed as necessary to perform the validation.
*
* @param username The username to validate.
* @param password The password to validate.
* @return true if the username/password pair given is authorized, false
* otherwise.
*/
public boolean validate(String username, String password) {
// If username matches
if (username != null && password != null
&& username.equals(this.username)) {
switch (encoding) {
// If plain text, just compare
case PLAIN_TEXT:
// Compare plaintext
return password.equals(this.password);
// If hased with MD5, hash password and compare
case MD5:
// Compare hashed password
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
String hashedPassword = getHexString(digest.digest(password.getBytes("UTF-8")));
return hashedPassword.equals(this.password.toUpperCase());
}
catch (UnsupportedEncodingException e) {
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
}
catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("Unexpected lack of MD5 support.", e);
}
}
} // end validation check
return false;
}
/**
* Returns the GuacamoleConfiguration having the given name and associated
* with the username/password pair stored within this authorization.
*
* @param name The name of the GuacamoleConfiguration to return.
* @return The GuacamoleConfiguration having the given name, or null if no
* such GuacamoleConfiguration exists.
*/
public GuacamoleConfiguration getConfiguration(String name) {
return configs.get(name);
}
/**
* Adds the given GuacamoleConfiguration to the set of stored configurations
* under the given name.
*
* @param name The name to associate this GuacamoleConfiguration with.
* @param config The GuacamoleConfiguration to store.
*/
public void addConfiguration(String name, GuacamoleConfiguration config) {
configs.put(name, config);
}
/**
* Returns a Map of all stored GuacamoleConfigurations associated with the
* username/password pair stored within this authorization, indexed by
* configuration name.
*
* @return A Map of all stored GuacamoleConfigurations.
*/
public Map<String, GuacamoleConfiguration> getConfigurations() {
return configs;
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.auth;
import java.util.HashMap;
import java.util.Map;
/**
* Mapping of all usernames to corresponding authorizations.
*
* @author Mike Jumper
*/
public class UserMapping {
/**
* All authorizations, indexed by username.
*/
private Map<String, Authorization> authorizations =
new HashMap<String, Authorization>();
/**
* Adds the given authorization to the user mapping.
*
* @param authorization The authorization to add to the user mapping.
*/
public void addAuthorization(Authorization authorization) {
authorizations.put(authorization.getUsername(), authorization);
}
/**
* Returns the authorization corresponding to the user having the given
* username, if any.
*
* @param username The username to find the authorization for.
* @return The authorization corresponding to the user having the given
* username, or null if no such authorization exists.
*/
public Authorization getAuthorization(String username) {
return authorizations.get(username);
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes which drive the default, basic authentication of the Guacamole
* web application.
*/
package org.apache.guacamole.net.basic.auth;
@@ -0,0 +1,203 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.extension;
import java.lang.reflect.InvocationTargetException;
import java.util.UUID;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides a safe wrapper around an AuthenticationProvider subclass, such that
* authentication attempts can cleanly fail, and errors can be properly logged,
* even if the AuthenticationProvider cannot be instantiated.
*
* @author Michael Jumper
*/
public class AuthenticationProviderFacade implements AuthenticationProvider {
/**
* Logger for this class.
*/
private Logger logger = LoggerFactory.getLogger(AuthenticationProviderFacade.class);
/**
* The underlying authentication provider, or null if the authentication
* provider could not be instantiated.
*/
private final AuthenticationProvider authProvider;
/**
* The identifier to provide for the underlying authentication provider if
* the authentication provider could not be loaded.
*/
private final String facadeIdentifier = UUID.randomUUID().toString();
/**
* Creates a new AuthenticationProviderFacade which delegates all function
* calls to an instance of the given AuthenticationProvider subclass. If
* an instance of the given class cannot be created, creation of this
* facade will still succeed, but its use will result in errors being
* logged, and all authentication attempts will fail.
*
* @param authProviderClass
* The AuthenticationProvider subclass to instantiate.
*/
public AuthenticationProviderFacade(Class<? extends AuthenticationProvider> authProviderClass) {
AuthenticationProvider instance = null;
try {
// Attempt to instantiate the authentication provider
instance = authProviderClass.getConstructor().newInstance();
}
catch (NoSuchMethodException e) {
logger.error("The authentication 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("AuthenticationProvider is missing a default constructor.", e);
}
catch (SecurityException e) {
logger.error("The Java security mananager is preventing authentication extensions "
+ "from being loaded. Please check the configuration of Java or your "
+ "servlet container.");
logger.debug("Creation of AuthenticationProvider disallowed by security manager.", e);
}
catch (InstantiationException e) {
logger.error("The authentication 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("AuthenticationProvider cannot be instantiated.", e);
}
catch (IllegalAccessException e) {
logger.error("The authentication 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 AuthenticationProvider is not public.", e);
}
catch (IllegalArgumentException e) {
logger.error("The authentication 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 AuthenticationProvider cannot accept zero arguments.", e);
}
catch (InvocationTargetException e) {
// Obtain causing error - create relatively-informative stub error if cause is unknown
Throwable cause = e.getCause();
if (cause == null)
cause = new GuacamoleException("Error encountered during initialization.");
logger.error("Authentication extension failed to start: {}", cause.getMessage());
logger.debug("AuthenticationProvider instantiation failed.", e);
}
// Associate instance, if any
authProvider = instance;
}
@Override
public String getIdentifier() {
// Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) {
logger.warn("The authentication system could not be loaded. Please check for errors earlier in the logs.");
return facadeIdentifier;
}
// Delegate to underlying auth provider
return authProvider.getIdentifier();
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) {
logger.warn("Authentication attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
}
// Delegate to underlying auth provider
return authProvider.authenticateUser(credentials);
}
@Override
public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException {
// Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) {
logger.warn("Reauthentication attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
}
// Delegate to underlying auth provider
return authProvider.updateAuthenticatedUser(authenticatedUser, credentials);
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) {
logger.warn("User data retrieval attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
return null;
}
// Delegate to underlying auth provider
return authProvider.getUserContext(authenticatedUser);
}
@Override
public UserContext updateUserContext(UserContext context,
AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// Ignore auth attempts if no auth provider could be loaded
if (authProvider == null) {
logger.warn("User data refresh attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
return null;
}
// Delegate to underlying auth provider
return authProvider.updateUserContext(context, authenticatedUser);
}
}
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.extension;
import java.io.File;
import java.io.FilenameFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.guacamole.GuacamoleException;
/**
* A ClassLoader implementation which finds classes within .jar files within a
* given directory.
*
* @author Michael Jumper
*/
public class DirectoryClassLoader extends URLClassLoader {
/**
* Returns an instance of DirectoryClassLoader configured to load .jar
* files from the given directory. Calling this function multiple times
* will not affect previously-returned instances of DirectoryClassLoader.
*
* @param dir
* The directory from which .jar files should be read.
*
* @return
* A DirectoryClassLoader instance which loads classes from the .jar
* files in the given directory.
*
* @throws GuacamoleException
* If the given file is not a directory, or the contents of the given
* directory cannot be read.
*/
public static DirectoryClassLoader getInstance(final File dir)
throws GuacamoleException {
try {
// Attempt to create singleton classloader which loads classes from
// all .jar's in the lib directory defined in guacamole.properties
return AccessController.doPrivileged(new PrivilegedExceptionAction<DirectoryClassLoader>() {
@Override
public DirectoryClassLoader run() throws GuacamoleException {
return new DirectoryClassLoader(dir);
}
});
}
catch (PrivilegedActionException e) {
throw (GuacamoleException) e.getException();
}
}
/**
* Returns all .jar files within the given directory as an array of URLs.
*
* @param dir
* The directory to retrieve all .jar files from.
*
* @return
* An array of the URLs of all .jar files within the given directory.
*
* @throws GuacamoleException
* If the given file is not a directory, or the contents of the given
* directory cannot be read.
*/
private static URL[] getJarURLs(File dir) throws GuacamoleException {
// Validate directory is indeed a directory
if (!dir.isDirectory())
throw new GuacamoleException(dir + " is not a directory.");
// Get list of URLs for all .jar's in the lib directory
Collection<URL> jarURLs = new ArrayList<URL>();
File[] files = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
// If it ends with .jar, accept the file
return name.endsWith(".jar");
}
});
// Verify directory was successfully read
if (files == null)
throw new GuacamoleException("Unable to read contents of directory " + dir);
// Add the URL for each .jar to the jar URL list
for (File file : files) {
try {
jarURLs.add(file.toURI().toURL());
}
catch (MalformedURLException e) {
throw new GuacamoleException(e);
}
}
// Set delegate classloader to new URLClassLoader which loads from the .jars found above.
URL[] urls = new URL[jarURLs.size()];
return jarURLs.toArray(urls);
}
/**
* Creates a new DirectoryClassLoader configured to load .jar files from
* the given directory.
*
* @param dir
* The directory from which .jar files should be read.
*
* @throws GuacamoleException
* If the given file is not a directory, or the contents of the given
* directory cannot be read.
*/
private DirectoryClassLoader(File dir) throws GuacamoleException {
super(getJarURLs(dir), DirectoryClassLoader.class.getClassLoader());
}
}
@@ -0,0 +1,515 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.extension;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.ObjectMapper;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.basic.resource.ClassPathResource;
import org.apache.guacamole.net.basic.resource.Resource;
/**
* A Guacamole extension, which may provide custom authentication, static
* files, theming/branding, etc.
*
* @author Michael Jumper
*/
public class Extension {
/**
* The Jackson parser for parsing the language JSON files.
*/
private static final ObjectMapper mapper = new ObjectMapper();
/**
* The name of the manifest file that describes the contents of a
* Guacamole extension.
*/
private static final String MANIFEST_NAME = "guac-manifest.json";
/**
* The parsed manifest file of this extension, describing the location of
* resources within the extension.
*/
private final ExtensionManifest manifest;
/**
* The classloader to use when reading resources from this extension,
* including classes and static files.
*/
private final ClassLoader classLoader;
/**
* Map of all JavaScript resources defined within the extension, where each
* key is the path to that resource within the extension.
*/
private final Map<String, Resource> javaScriptResources;
/**
* Map of all CSS resources defined within the extension, where each key is
* the path to that resource within the extension.
*/
private final Map<String, Resource> cssResources;
/**
* Map of all HTML patch resources defined within the extension, where each
* key is the path to that resource within the extension.
*/
private final Map<String, Resource> htmlResources;
/**
* Map of all translation resources defined within the extension, where
* each key is the path to that resource within the extension.
*/
private final Map<String, Resource> translationResources;
/**
* Map of all resources defined within the extension which are not already
* associated as JavaScript, CSS, or translation resources, where each key
* is the path to that resource within the extension.
*/
private final Map<String, Resource> staticResources;
/**
* The collection of all AuthenticationProvider classes defined within the
* extension.
*/
private final Collection<Class<AuthenticationProvider>> authenticationProviderClasses;
/**
* The resource for the small favicon for the extension. If provided, this
* will replace the default Guacamole icon.
*/
private final Resource smallIcon;
/**
* The resource foe the large favicon for the extension. If provided, this
* will replace the default Guacamole icon.
*/
private final Resource largeIcon;
/**
* Returns a new map of all resources corresponding to the collection of
* paths provided. Each resource will be associated with the given
* mimetype, and stored in the map using its path as the key.
*
* @param mimetype
* The mimetype to associate with each resource.
*
* @param paths
* The paths corresponding to the resources desired.
*
* @return
* A new, unmodifiable map of resources corresponding to the
* collection of paths provided, where the key of each entry in the
* map is the path for the resource stored in that entry.
*/
private Map<String, Resource> getClassPathResources(String mimetype, Collection<String> paths) {
// If no paths are provided, just return an empty map
if (paths == null)
return Collections.<String, Resource>emptyMap();
// Add classpath resource for each path provided
Map<String, Resource> resources = new HashMap<String, Resource>(paths.size());
for (String path : paths)
resources.put(path, new ClassPathResource(classLoader, mimetype, path));
// Callers should not rely on modifying the result
return Collections.unmodifiableMap(resources);
}
/**
* Returns a new map of all resources corresponding to the map of resource
* paths provided. Each resource will be associated with the mimetype
* stored in the given map using its path as the key.
*
* @param resourceTypes
* A map of all paths to their corresponding mimetypes.
*
* @return
* A new, unmodifiable map of resources corresponding to the
* collection of paths provided, where the key of each entry in the
* map is the path for the resource stored in that entry.
*/
private Map<String, Resource> getClassPathResources(Map<String, String> resourceTypes) {
// If no paths are provided, just return an empty map
if (resourceTypes == null)
return Collections.<String, Resource>emptyMap();
// Add classpath resource for each path/mimetype pair provided
Map<String, Resource> resources = new HashMap<String, Resource>(resourceTypes.size());
for (Map.Entry<String, String> resource : resourceTypes.entrySet()) {
// Get path and mimetype from entry
String path = resource.getKey();
String mimetype = resource.getValue();
// Store as path/resource pair
resources.put(path, new ClassPathResource(classLoader, mimetype, path));
}
// Callers should not rely on modifying the result
return Collections.unmodifiableMap(resources);
}
/**
* Retrieve the AuthenticationProvider subclass having the given name. If
* the class having the given name does not exist or isn't actually a
* subclass of AuthenticationProvider, an exception will be thrown.
*
* @param name
* The name of the AuthenticationProvider class to retrieve.
*
* @return
* The subclass of AuthenticationProvider having the given name.
*
* @throws GuacamoleException
* If no such class exists, or if the class with the given name is not
* a subclass of AuthenticationProvider.
*/
@SuppressWarnings("unchecked") // We check this ourselves with isAssignableFrom()
private Class<AuthenticationProvider> getAuthenticationProviderClass(String name)
throws GuacamoleException {
try {
// Get authentication provider class
Class<?> authenticationProviderClass = classLoader.loadClass(name);
// Verify the located class is actually a subclass of AuthenticationProvider
if (!AuthenticationProvider.class.isAssignableFrom(authenticationProviderClass))
throw new GuacamoleServerException("Authentication providers MUST extend the AuthenticationProvider class.");
// Return located class
return (Class<AuthenticationProvider>) authenticationProviderClass;
}
catch (ClassNotFoundException e) {
throw new GuacamoleException("Authentication provider class not found.", e);
}
}
/**
* Returns a new collection of all AuthenticationProvider subclasses having
* the given names. If any class does not exist or isn't actually a
* subclass of AuthenticationProvider, an exception will be thrown, and
* no further AuthenticationProvider classes will be loaded.
*
* @param names
* The names of the AuthenticationProvider classes to retrieve.
*
* @return
* A new collection of all AuthenticationProvider subclasses having the
* given names.
*
* @throws GuacamoleException
* If any given class does not exist, or if any given class is not a
* subclass of AuthenticationProvider.
*/
private Collection<Class<AuthenticationProvider>> getAuthenticationProviderClasses(Collection<String> names)
throws GuacamoleException {
// If no classnames are provided, just return an empty list
if (names == null)
return Collections.<Class<AuthenticationProvider>>emptyList();
// Define all auth provider classes
Collection<Class<AuthenticationProvider>> classes = new ArrayList<Class<AuthenticationProvider>>(names.size());
for (String name : names)
classes.add(getAuthenticationProviderClass(name));
// Callers should not rely on modifying the result
return Collections.unmodifiableCollection(classes);
}
/**
* Loads the given file as an extension, which must be a .jar containing
* a guac-manifest.json file describing its contents.
*
* @param parent
* The classloader to use as the parent for the isolated classloader of
* this extension.
*
* @param file
* The file to load as an extension.
*
* @throws GuacamoleException
* If the provided file is not a .jar file, does not contain the
* guac-manifest.json, or if guac-manifest.json is invalid and cannot
* be parsed.
*/
public Extension(final ClassLoader parent, final File file) throws GuacamoleException {
try {
// Open extension
ZipFile extension = new ZipFile(file);
try {
// Retrieve extension manifest
ZipEntry manifestEntry = extension.getEntry(MANIFEST_NAME);
if (manifestEntry == null)
throw new GuacamoleServerException("Extension " + file.getName() + " is missing " + MANIFEST_NAME);
// Parse manifest
manifest = mapper.readValue(extension.getInputStream(manifestEntry), ExtensionManifest.class);
if (manifest == null)
throw new GuacamoleServerException("Contents of " + MANIFEST_NAME + " must be a valid JSON object.");
}
// Always close zip file, if possible
finally {
extension.close();
}
try {
// Create isolated classloader for this extension
classLoader = AccessController.doPrivileged(new PrivilegedExceptionAction<ClassLoader>() {
@Override
public ClassLoader run() throws GuacamoleException {
try {
// Classloader must contain only the extension itself
return new URLClassLoader(new URL[]{file.toURI().toURL()}, parent);
}
catch (MalformedURLException e) {
throw new GuacamoleException(e);
}
}
});
}
// Rethrow any GuacamoleException
catch (PrivilegedActionException e) {
throw (GuacamoleException) e.getException();
}
}
// Abort load if not a valid zip file
catch (ZipException e) {
throw new GuacamoleServerException("Extension is not a valid zip file: " + file.getName(), e);
}
// Abort if manifest cannot be parsed (invalid JSON)
catch (JsonParseException e) {
throw new GuacamoleServerException(MANIFEST_NAME + " is not valid JSON: " + file.getName(), e);
}
// Abort if zip file cannot be read at all due to I/O errors
catch (IOException e) {
throw new GuacamoleServerException("Unable to read extension: " + file.getName(), e);
}
// Define static resources
cssResources = getClassPathResources("text/css", manifest.getCSSPaths());
javaScriptResources = getClassPathResources("text/javascript", manifest.getJavaScriptPaths());
htmlResources = getClassPathResources("text/html", manifest.getHTMLPaths());
translationResources = getClassPathResources("application/json", manifest.getTranslationPaths());
staticResources = getClassPathResources(manifest.getResourceTypes());
// Define authentication providers
authenticationProviderClasses = getAuthenticationProviderClasses(manifest.getAuthProviders());
// Get small icon resource if provided
if (manifest.getSmallIcon() != null)
smallIcon = new ClassPathResource(classLoader, "image/png", manifest.getSmallIcon());
else
smallIcon = null;
// Get large icon resource if provided
if (manifest.getLargeIcon() != null)
largeIcon = new ClassPathResource(classLoader, "image/png", manifest.getLargeIcon());
else
largeIcon = null;
}
/**
* Returns the version of the Guacamole web application for which this
* extension was built.
*
* @return
* The version of the Guacamole web application for which this
* extension was built.
*/
public String getGuacamoleVersion() {
return manifest.getGuacamoleVersion();
}
/**
* Returns the name of this extension, as declared in the extension's
* manifest.
*
* @return
* The name of this extension.
*/
public String getName() {
return manifest.getName();
}
/**
* Returns the namespace of this extension, as declared in the extension's
* manifest.
*
* @return
* The namespace of this extension.
*/
public String getNamespace() {
return manifest.getNamespace();
}
/**
* Returns a map of all declared JavaScript resources associated with this
* extension, where the key of each entry in the map is the path to that
* resource within the extension .jar. JavaScript resources are declared
* within the extension manifest.
*
* @return
* All declared JavaScript resources associated with this extension.
*/
public Map<String, Resource> getJavaScriptResources() {
return javaScriptResources;
}
/**
* Returns a map of all declared CSS resources associated with this
* extension, where the key of each entry in the map is the path to that
* resource within the extension .jar. CSS resources are declared within
* the extension manifest.
*
* @return
* All declared CSS resources associated with this extension.
*/
public Map<String, Resource> getCSSResources() {
return cssResources;
}
/**
* Returns a map of all declared HTML patch resources associated with this
* extension, where the key of each entry in the map is the path to that
* resource within the extension .jar. HTML patch resources are declared
* within the extension manifest.
*
* @return
* All declared HTML patch resources associated with this extension.
*/
public Map<String, Resource> getHTMLResources() {
return htmlResources;
}
/**
* Returns a map of all declared translation resources associated with this
* extension, where the key of each entry in the map is the path to that
* resource within the extension .jar. Translation resources are declared
* within the extension manifest.
*
* @return
* All declared translation resources associated with this extension.
*/
public Map<String, Resource> getTranslationResources() {
return translationResources;
}
/**
* Returns a map of all declared resources associated with this extension,
* where these resources are not already associated as JavaScript, CSS, or
* translation resources. The key of each entry in the map is the path to
* that resource within the extension .jar. Static resources are declared
* within the extension manifest.
*
* @return
* All declared static resources associated with this extension.
*/
public Map<String, Resource> getStaticResources() {
return staticResources;
}
/**
* Returns all declared authentication providers classes associated with
* this extension. Authentication providers are declared within the
* extension manifest.
*
* @return
* All declared authentication provider classes with this extension.
*/
public Collection<Class<AuthenticationProvider>> getAuthenticationProviderClasses() {
return authenticationProviderClasses;
}
/**
* Returns the resource for the small favicon for the extension. If
* provided, this will replace the default Guacamole icon.
*
* @return
* The resource for the small favicon.
*/
public Resource getSmallIcon() {
return smallIcon;
}
/**
* Returns the resource for the large favicon for the extension. If
* provided, this will replace the default Guacamole icon.
*
* @return
* The resource for the large favicon.
*/
public Resource getLargeIcon() {
return largeIcon;
}
}
@@ -0,0 +1,407 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.extension;
import java.util.Collection;
import java.util.Map;
import org.codehaus.jackson.annotate.JsonProperty;
/**
* Java representation of the JSON manifest contained within every Guacamole
* extension, identifying an extension and describing its contents.
*
* @author Michael Jumper
*/
public class ExtensionManifest {
/**
* The version of Guacamole for which this extension was built.
* Compatibility rules built into the web application will guard against
* incompatible extensions being loaded.
*/
private String guacamoleVersion;
/**
* The name of the extension associated with this manifest. The extension
* name is human-readable, and used for display purposes only.
*/
private String name;
/**
* The namespace of the extension associated with this manifest. The
* extension namespace is required for internal use, and is used wherever
* extension-specific files or resources need to be isolated from those of
* other extensions.
*/
private String namespace;
/**
* The paths of all JavaScript resources within the .jar of the extension
* associated with this manifest.
*/
private Collection<String> javaScriptPaths;
/**
* The paths of all CSS resources within the .jar of the extension
* associated with this manifest.
*/
private Collection<String> cssPaths;
/**
* The paths of all HTML patch resources within the .jar of the extension
* associated with this manifest.
*/
private Collection<String> htmlPaths;
/**
* The paths of all translation JSON files within this extension, if any.
*/
private Collection<String> translationPaths;
/**
* The mimetypes of all resources within this extension which are not
* already declared as JavaScript, CSS, or translation resources, if any.
* The key of each entry is the resource path, while the value is the
* corresponding mimetype.
*/
private Map<String, String> resourceTypes;
/**
* The names of all authentication provider classes within this extension,
* if any.
*/
private Collection<String> authProviders;
/**
* The path to the small favicon. If provided, this will replace the default
* Guacamole icon.
*/
private String smallIcon;
/**
* The path to the large favicon. If provided, this will replace the default
* Guacamole icon.
*/
private String largeIcon;
/**
* Returns the version of the Guacamole web application for which the
* extension was built, such as "0.9.7".
*
* @return
* The version of the Guacamole web application for which the extension
* was built.
*/
public String getGuacamoleVersion() {
return guacamoleVersion;
}
/**
* Sets the version of the Guacamole web application for which the
* extension was built, such as "0.9.7".
*
* @param guacamoleVersion
* The version of the Guacamole web application for which the extension
* was built.
*/
public void setGuacamoleVersion(String guacamoleVersion) {
this.guacamoleVersion = guacamoleVersion;
}
/**
* Returns the name of the extension associated with this manifest. The
* name is human-readable, for display purposes only, and is defined within
* the manifest by the "name" property.
*
* @return
* The name of the extension associated with this manifest.
*/
public String getName() {
return name;
}
/**
* Sets the name of the extension associated with this manifest. The name
* is human-readable, for display purposes only, and is defined within the
* manifest by the "name" property.
*
* @param name
* The name of the extension associated with this manifest.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns the namespace of the extension associated with this manifest.
* The namespace is required for internal use, and is used wherever
* extension-specific files or resources need to be isolated from those of
* other extensions. It is defined within the manifest by the "namespace"
* property.
*
* @return
* The namespace of the extension associated with this manifest.
*/
public String getNamespace() {
return namespace;
}
/**
* Sets the namespace of the extension associated with this manifest. The
* namespace is required for internal use, and is used wherever extension-
* specific files or resources need to be isolated from those of other
* extensions. It is defined within the manifest by the "namespace"
* property.
*
* @param namespace
* The namespace of the extension associated with this manifest.
*/
public void setNamespace(String namespace) {
this.namespace = namespace;
}
/**
* Returns the paths to all JavaScript resources within the extension.
* These paths are defined within the manifest by the "js" property as an
* array of strings, where each string is a path relative to the root of
* the extension .jar.
*
* @return
* A collection of paths to all JavaScript resources within the
* extension.
*/
@JsonProperty("js")
public Collection<String> getJavaScriptPaths() {
return javaScriptPaths;
}
/**
* Sets the paths to all JavaScript resources within the extension. These
* paths are defined within the manifest by the "js" property as an array
* of strings, where each string is a path relative to the root of the
* extension .jar.
*
* @param javaScriptPaths
* A collection of paths to all JavaScript resources within the
* extension.
*/
@JsonProperty("js")
public void setJavaScriptPaths(Collection<String> javaScriptPaths) {
this.javaScriptPaths = javaScriptPaths;
}
/**
* Returns the paths to all CSS resources within the extension. These paths
* are defined within the manifest by the "js" property as an array of
* strings, where each string is a path relative to the root of the
* extension .jar.
*
* @return
* A collection of paths to all CSS resources within the extension.
*/
@JsonProperty("css")
public Collection<String> getCSSPaths() {
return cssPaths;
}
/**
* Sets the paths to all CSS resources within the extension. These paths
* are defined within the manifest by the "js" property as an array of
* strings, where each string is a path relative to the root of the
* extension .jar.
*
* @param cssPaths
* A collection of paths to all CSS resources within the extension.
*/
@JsonProperty("css")
public void setCSSPaths(Collection<String> cssPaths) {
this.cssPaths = cssPaths;
}
/**
* Returns the paths to all HTML patch resources within the extension. These
* paths are defined within the manifest by the "html" property as an array
* of strings, where each string is a path relative to the root of the
* extension .jar.
*
* @return
* A collection of paths to all HTML patch resources within the
* extension.
*/
@JsonProperty("html")
public Collection<String> getHTMLPaths() {
return htmlPaths;
}
/**
* Sets the paths to all HTML patch resources within the extension. These
* paths are defined within the manifest by the "html" property as an array
* of strings, where each string is a path relative to the root of the
* extension .jar.
*
* @param htmlPatchPaths
* A collection of paths to all HTML patch resources within the
* extension.
*/
@JsonProperty("html")
public void setHTMLPaths(Collection<String> htmlPatchPaths) {
this.htmlPaths = htmlPatchPaths;
}
/**
* Returns the paths to all translation resources within the extension.
* These paths are defined within the manifest by the "translations"
* property as an array of strings, where each string is a path relative to
* the root of the extension .jar.
*
* @return
* A collection of paths to all translation resources within the
* extension.
*/
@JsonProperty("translations")
public Collection<String> getTranslationPaths() {
return translationPaths;
}
/**
* Sets the paths to all translation resources within the extension. These
* paths are defined within the manifest by the "translations" property as
* an array of strings, where each string is a path relative to the root of
* the extension .jar.
*
* @param translationPaths
* A collection of paths to all translation resources within the
* extension.
*/
@JsonProperty("translations")
public void setTranslationPaths(Collection<String> translationPaths) {
this.translationPaths = translationPaths;
}
/**
* Returns a map of all resources to their corresponding mimetypes, for all
* resources not already declared as JavaScript, CSS, or translation
* resources. These paths and corresponding types are defined within the
* manifest by the "resources" property as an object, where each property
* name is a path relative to the root of the extension .jar, and each
* value is a mimetype.
*
* @return
* A map of all resources within the extension to their corresponding
* mimetypes.
*/
@JsonProperty("resources")
public Map<String, String> getResourceTypes() {
return resourceTypes;
}
/**
* Sets the map of all resources to their corresponding mimetypes, for all
* resources not already declared as JavaScript, CSS, or translation
* resources. These paths and corresponding types are defined within the
* manifest by the "resources" property as an object, where each property
* name is a path relative to the root of the extension .jar, and each
* value is a mimetype.
*
* @param resourceTypes
* A map of all resources within the extension to their corresponding
* mimetypes.
*/
@JsonProperty("resources")
public void setResourceTypes(Map<String, String> resourceTypes) {
this.resourceTypes = resourceTypes;
}
/**
* Returns the classnames of all authentication provider classes within the
* extension. These classnames are defined within the manifest by the
* "authProviders" property as an array of strings, where each string is an
* authentication provider classname.
*
* @return
* A collection of classnames of all authentication providers within
* the extension.
*/
public Collection<String> getAuthProviders() {
return authProviders;
}
/**
* Sets the classnames of all authentication provider classes within the
* extension. These classnames are defined within the manifest by the
* "authProviders" property as an array of strings, where each string is an
* authentication provider classname.
*
* @param authProviders
* A collection of classnames of all authentication providers within
* the extension.
*/
public void setAuthProviders(Collection<String> authProviders) {
this.authProviders = authProviders;
}
/**
* Returns the path to the small favicon, relative to the root of the
* extension.
*
* @return
* The path to the small favicon.
*/
public String getSmallIcon() {
return smallIcon;
}
/**
* Sets the path to the small favicon. This will replace the default
* Guacamole icon.
*
* @param smallIcon
* The path to the small favicon.
*/
public void setSmallIcon(String smallIcon) {
this.smallIcon = smallIcon;
}
/**
* Returns the path to the large favicon, relative to the root of the
* extension.
*
* @return
* The path to the large favicon.
*/
public String getLargeIcon() {
return largeIcon;
}
/**
* Sets the path to the large favicon. This will replace the default
* Guacamole icon.
*
* @param largeIcon
* The path to the large favicon.
*/
public void setLargeIcon(String largeIcon) {
this.largeIcon = largeIcon;
}
}
@@ -0,0 +1,450 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.extension;
import com.google.inject.Provides;
import com.google.inject.servlet.ServletModule;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import net.sourceforge.guacamole.net.basic.BasicFileAuthenticationProvider;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.basic.properties.BasicGuacamoleProperties;
import org.apache.guacamole.net.basic.resource.Resource;
import org.apache.guacamole.net.basic.resource.ResourceServlet;
import org.apache.guacamole.net.basic.resource.SequenceResource;
import org.apache.guacamole.net.basic.resource.WebApplicationResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A Guice Module which loads all extensions within the
* GUACAMOLE_HOME/extensions directory, if any.
*
* @author Michael Jumper
*/
public class ExtensionModule extends ServletModule {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(ExtensionModule.class);
/**
* The version strings of all Guacamole versions whose extensions are
* compatible with this release.
*/
private static final List<String> ALLOWED_GUACAMOLE_VERSIONS =
Collections.unmodifiableList(Arrays.asList(
"*",
"0.9.9"
));
/**
* The name of the directory within GUACAMOLE_HOME containing any .jars
* which should be included in the classpath of all extensions.
*/
private static final String LIB_DIRECTORY = "lib";
/**
* The name of the directory within GUACAMOLE_HOME containing all
* extensions.
*/
private static final String EXTENSIONS_DIRECTORY = "extensions";
/**
* The string that the filenames of all extensions must end with to be
* recognized as extensions.
*/
private static final String EXTENSION_SUFFIX = ".jar";
/**
* The Guacamole server environment.
*/
private final Environment environment;
/**
* All currently-bound authentication providers, if any.
*/
private final List<AuthenticationProvider> boundAuthenticationProviders =
new ArrayList<AuthenticationProvider>();
/**
* Service for adding and retrieving language resources.
*/
private final LanguageResourceService languageResourceService;
/**
* Service for adding and retrieving HTML patch resources.
*/
private final PatchResourceService patchResourceService;
/**
* Returns the classloader that should be used as the parent classloader
* for all extensions. If the GUACAMOLE_HOME/lib directory exists, this
* will be a classloader that loads classes from within the .jar files in
* that directory. Lacking the GUACAMOLE_HOME/lib directory, this will
* simply be the classloader associated with the ExtensionModule class.
*
* @return
* The classloader that should be used as the parent classloader for
* all extensions.
*
* @throws GuacamoleException
* If an error occurs while retrieving the classloader.
*/
private ClassLoader getParentClassLoader() throws GuacamoleException {
// Retrieve lib directory
File libDir = new File(environment.getGuacamoleHome(), LIB_DIRECTORY);
// If lib directory does not exist, use default class loader
if (!libDir.isDirectory())
return ExtensionModule.class.getClassLoader();
// Return classloader which loads classes from all .jars within the lib directory
return DirectoryClassLoader.getInstance(libDir);
}
/**
* Creates a module which loads all extensions within the
* GUACAMOLE_HOME/extensions directory.
*
* @param environment
* The environment to use when configuring authentication.
*/
public ExtensionModule(Environment environment) {
this.environment = environment;
this.languageResourceService = new LanguageResourceService(environment);
this.patchResourceService = new PatchResourceService();
}
/**
* 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, along with any other bound AuthenticationProviders.
*
* @param authenticationProvider
* The AuthenticationProvider class to bind.
*/
private void bindAuthenticationProvider(Class<? extends AuthenticationProvider> authenticationProvider) {
// Bind authentication provider
logger.debug("[{}] Binding AuthenticationProvider \"{}\".",
boundAuthenticationProviders.size(), authenticationProvider.getName());
boundAuthenticationProviders.add(new AuthenticationProviderFacade(authenticationProvider));
}
/**
* Binds each of the the given AuthenticationProvider classes such that any
* service requiring access to the AuthenticationProvider can obtain it via
* injection.
*
* @param authProviders
* The AuthenticationProvider classes to bind.
*/
private void bindAuthenticationProviders(Collection<Class<AuthenticationProvider>> authProviders) {
// Bind each authentication provider within extension
for (Class<AuthenticationProvider> authenticationProvider : authProviders)
bindAuthenticationProvider(authenticationProvider);
}
/**
* Returns a list of all currently-bound AuthenticationProvider instances.
*
* @return
* A List of all currently-bound AuthenticationProvider. The List is
* not modifiable.
*/
@Provides
public List<AuthenticationProvider> getAuthenticationProviders() {
return Collections.unmodifiableList(boundAuthenticationProviders);
}
/**
* Serves each of the given resources as a language resource. Language
* resources are served from within the "/translations" directory as JSON
* files, where the name of each JSON file is the language key.
*
* @param resources
* A map of all language resources to serve, where the key of each
* entry in the language key from which the name of the JSON file will
* be derived.
*/
private void serveLanguageResources(Map<String, Resource> resources) {
// Add all resources to language resource service
for (Map.Entry<String, Resource> translationResource : resources.entrySet()) {
// Get path and resource from path/resource pair
String path = translationResource.getKey();
Resource resource = translationResource.getValue();
// Derive key from path
String languageKey = languageResourceService.getLanguageKey(path);
if (languageKey == null) {
logger.warn("Invalid language file name: \"{}\"", path);
continue;
}
// Add language resource
languageResourceService.addLanguageResource(languageKey, resource);
}
}
/**
* Serves each of the given resources under the given prefix. The path of
* each resource relative to the prefix is the key of its entry within the
* map.
*
* @param prefix
* The prefix under which each resource should be served.
*
* @param resources
* A map of all resources to serve, where the key of each entry in the
* map is the desired path of that resource relative to the prefix.
*/
private void serveStaticResources(String prefix, Map<String, Resource> resources) {
// Add all resources under given prefix
for (Map.Entry<String, Resource> staticResource : resources.entrySet()) {
// Get path and resource from path/resource pair
String path = staticResource.getKey();
Resource resource = staticResource.getValue();
// Serve within namespace-derived path
serve(prefix + path).with(new ResourceServlet(resource));
}
}
/**
* Returns whether the given version of Guacamole is compatible with this
* version of Guacamole as far as extensions are concerned.
*
* @param guacamoleVersion
* The version of Guacamole the extension was built for.
*
* @return
* true if the given version of Guacamole is compatible with this
* version of Guacamole, false otherwise.
*/
private boolean isCompatible(String guacamoleVersion) {
return ALLOWED_GUACAMOLE_VERSIONS.contains(guacamoleVersion);
}
/**
* Loads all extensions within the GUACAMOLE_HOME/extensions directory, if
* any, adding their static resource to the given resoure collections.
*
* @param javaScriptResources
* A modifiable collection of static JavaScript resources which may
* receive new JavaScript resources from extensions.
*
* @param cssResources
* A modifiable collection of static CSS resources which may receive
* new CSS resources from extensions.
*/
private void loadExtensions(Collection<Resource> javaScriptResources,
Collection<Resource> cssResources) {
// Retrieve and validate extensions directory
File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
if (!extensionsDir.isDirectory())
return;
// Retrieve list of all extension files within extensions directory
File[] extensionFiles = extensionsDir.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().endsWith(EXTENSION_SUFFIX);
}
});
// Verify contents are accessible
if (extensionFiles == null) {
logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read.");
return;
}
// Sort files lexicographically
Arrays.sort(extensionFiles);
// Load each extension within the extension directory
for (File extensionFile : extensionFiles) {
logger.debug("Loading extension: \"{}\"", extensionFile.getName());
try {
// Load extension from file
Extension extension = new Extension(getParentClassLoader(), extensionFile);
// Validate Guacamole version of extension
if (!isCompatible(extension.getGuacamoleVersion())) {
logger.debug("Declared Guacamole version \"{}\" of extension \"{}\" is not compatible with this version of Guacamole.",
extension.getGuacamoleVersion(), extensionFile.getName());
throw new GuacamoleServerException("Extension \"" + extension.getName() + "\" is not "
+ "compatible with this version of Guacamole.");
}
// Add any JavaScript / CSS resources
javaScriptResources.addAll(extension.getJavaScriptResources().values());
cssResources.addAll(extension.getCSSResources().values());
// Attempt to load all authentication providers
bindAuthenticationProviders(extension.getAuthenticationProviderClasses());
// Add any translation resources
serveLanguageResources(extension.getTranslationResources());
// Add all HTML patch resources
patchResourceService.addPatchResources(extension.getHTMLResources().values());
// Add all static resources under namespace-derived prefix
String staticResourcePrefix = "/app/ext/" + extension.getNamespace() + "/";
serveStaticResources(staticResourcePrefix, extension.getStaticResources());
// Serve up the small favicon if provided
if(extension.getSmallIcon() != null)
serve("/images/logo-64.png").with(new ResourceServlet(extension.getSmallIcon()));
// Serve up the large favicon if provided
if(extension.getLargeIcon()!= null)
serve("/images/logo-144.png").with(new ResourceServlet(extension.getLargeIcon()));
// Log successful loading of extension by name
logger.info("Extension \"{}\" loaded.", extension.getName());
}
catch (GuacamoleException e) {
logger.error("Extension \"{}\" could not be loaded: {}", extensionFile.getName(), e.getMessage());
logger.debug("Unable to load extension.", e);
}
}
}
@Override
protected void configureServlets() {
// Bind resource services
bind(LanguageResourceService.class).toInstance(languageResourceService);
bind(PatchResourceService.class).toInstance(patchResourceService);
// Load initial language resources from servlet context
languageResourceService.addLanguageResources(getServletContext());
// Load authentication provider from guacamole.properties for sake of backwards compatibility
Class<AuthenticationProvider> authProviderProperty = getAuthProviderProperty();
if (authProviderProperty != null)
bindAuthenticationProvider(authProviderProperty);
// Init JavaScript resources with base guacamole.min.js
Collection<Resource> javaScriptResources = new ArrayList<Resource>();
javaScriptResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.js"));
// Init CSS resources with base guacamole.min.css
Collection<Resource> cssResources = new ArrayList<Resource>();
cssResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.css"));
// Load all extensions
loadExtensions(javaScriptResources, cssResources);
// Always bind basic auth last
bindAuthenticationProvider(BasicFileAuthenticationProvider.class);
// Dynamically generate app.js and app.css from extensions
serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources)));
serve("/app.css").with(new ResourceServlet(new SequenceResource(cssResources)));
// Dynamically serve all language resources
for (Map.Entry<String, Resource> entry : languageResourceService.getLanguageResources().entrySet()) {
// Get language key/resource pair
String languageKey = entry.getKey();
Resource resource = entry.getValue();
// Serve resource within /translations
serve("/translations/" + languageKey + ".json").with(new ResourceServlet(resource));
}
}
}
@@ -0,0 +1,442 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.extension;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletContext;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.net.basic.properties.BasicGuacamoleProperties;
import org.apache.guacamole.net.basic.resource.ByteArrayResource;
import org.apache.guacamole.net.basic.resource.Resource;
import org.apache.guacamole.net.basic.resource.WebApplicationResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service which provides access to all built-in languages as resources, and
* allows other resources to be added or overlaid against existing resources.
*
* @author Michael Jumper
*/
public class LanguageResourceService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(LanguageResourceService.class);
/**
* The path to the translation folder within the webapp.
*/
private static final String TRANSLATION_PATH = "/translations";
/**
* The JSON property for the human readable display name.
*/
private static final String LANGUAGE_DISPLAY_NAME_KEY = "NAME";
/**
* The Jackson parser for parsing the language JSON files.
*/
private static final ObjectMapper mapper = new ObjectMapper();
/**
* The regular expression to use for parsing the language key from the
* filename.
*/
private static final Pattern LANGUAGE_KEY_PATTERN = Pattern.compile(".*/([a-z]+(_[A-Z]+)?)\\.json");
/**
* The set of all language keys which are explicitly listed as allowed
* within guacamole.properties, or null if all defined languages should be
* allowed.
*/
private final Set<String> allowedLanguages;
/**
* Map of all language resources by language key. Language keys are
* language and country code pairs, separated by an underscore, like
* "en_US". The country code and underscore SHOULD be omitted in the case
* that only one dialect of that language is defined, or in the case of the
* most universal or well-supported of all supported dialects of that
* language.
*/
private final Map<String, Resource> resources = new HashMap<String, Resource>();
/**
* Creates a new service for tracking and parsing available translations
* which reads its configuration from the given environment.
*
* @param environment
* The environment from which the configuration properties of this
* service should be read.
*/
public LanguageResourceService(Environment environment) {
Set<String> parsedAllowedLanguages;
// Parse list of available languages from properties
try {
parsedAllowedLanguages = environment.getProperty(BasicGuacamoleProperties.ALLOWED_LANGUAGES);
logger.debug("Available languages will be restricted to: {}", parsedAllowedLanguages);
}
// 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);
}
this.allowedLanguages = parsedAllowedLanguages;
}
/**
* Derives a language key from the filename within the given path, if
* possible. If the filename is not a valid language key, null is returned.
*
* @param path
* The path containing the filename to derive the language key from.
*
* @return
* The derived language key, or null if the filename is not a valid
* language key.
*/
public String getLanguageKey(String path) {
// Parse language key from filename
Matcher languageKeyMatcher = LANGUAGE_KEY_PATTERN.matcher(path);
if (!languageKeyMatcher.matches())
return null;
// Return parsed key
return languageKeyMatcher.group(1);
}
/**
* Merges the given JSON objects. Any leaf node in overlay will overwrite
* the corresponding path in original.
*
* @param original
* The original JSON object to which changes should be applied.
*
* @param overlay
* The JSON object containing changes that should be applied.
*
* @return
* The newly constructed JSON object that is the result of merging
* original and overlay.
*/
private JsonNode mergeTranslations(JsonNode original, JsonNode overlay) {
// If we are at a leaf node, the result of merging is simply the overlay
if (!overlay.isObject() || original == null)
return overlay;
// Create mutable copy of original
ObjectNode newNode = JsonNodeFactory.instance.objectNode();
Iterator<String> fieldNames = original.getFieldNames();
while (fieldNames.hasNext()) {
String fieldName = fieldNames.next();
newNode.put(fieldName, original.get(fieldName));
}
// Merge each field
fieldNames = overlay.getFieldNames();
while (fieldNames.hasNext()) {
String fieldName = fieldNames.next();
newNode.put(fieldName, mergeTranslations(original.get(fieldName), overlay.get(fieldName)));
}
return newNode;
}
/**
* Parses the given language resource, returning the resulting JsonNode.
* If the resource cannot be read because it does not exist, null is
* returned.
*
* @param resource
* The language resource to parse. Language resources must have the
* mimetype "application/json".
*
* @return
* A JsonNode representing the root of the parsed JSON tree, or null if
* the given resource does not exist.
*
* @throws IOException
* If an error occurs while parsing the resource as JSON.
*/
private JsonNode parseLanguageResource(Resource resource) throws IOException {
// Get resource stream
InputStream stream = resource.asStream();
if (stream == null)
return null;
// Parse JSON tree
try {
JsonNode tree = mapper.readTree(stream);
return tree;
}
// Ensure stream is always closed
finally {
stream.close();
}
}
/**
* Returns whether a language having the given key should be allowed to be
* loaded. If language availability restrictions are imposed through
* guacamole.properties, this may return false in some cases. By default,
* this function will always return true. Note that just because a language
* key is allowed to be loaded does not imply that the language key is
* valid.
*
* @param languageKey
* The language key of the language to test.
*
* @return
* true if the given language key should be allowed to be loaded, false
* otherwise.
*/
private boolean isLanguageAllowed(String languageKey) {
// If no list is provided, all languages are implicitly available
if (allowedLanguages == null)
return true;
return allowedLanguages.contains(languageKey);
}
/**
* Adds or overlays the given language resource, which need not exist in
* the ServletContext. If a language resource is already defined for the
* given language key, the strings from the given resource will be overlaid
* on top of the existing strings, augmenting or overriding the available
* strings for that language.
*
* @param key
* The language key of the resource being added. Language keys are
* pairs consisting of a language code followed by an underscore and
* country code, such as "en_US".
*
* @param resource
* The language resource to add. This resource must have the mimetype
* "application/json".
*/
public void addLanguageResource(String key, Resource resource) {
// Skip loading of language if not allowed
if (!isLanguageAllowed(key)) {
logger.debug("OMITTING language: \"{}\"", key);
return;
}
// Merge language resources if already defined
Resource existing = resources.get(key);
if (existing != null) {
try {
// Read the original language resource
JsonNode existingTree = parseLanguageResource(existing);
if (existingTree == null) {
logger.warn("Base language resource \"{}\" does not exist.", key);
return;
}
// Read new language resource
JsonNode resourceTree = parseLanguageResource(resource);
if (resourceTree == null) {
logger.warn("Overlay language resource \"{}\" does not exist.", key);
return;
}
// Merge the language resources
JsonNode mergedTree = mergeTranslations(existingTree, resourceTree);
resources.put(key, new ByteArrayResource("application/json", mapper.writeValueAsBytes(mergedTree)));
logger.debug("Merged strings with existing language: \"{}\"", key);
}
catch (IOException e) {
logger.error("Unable to merge language resource \"{}\": {}", key, e.getMessage());
logger.debug("Error merging language resource.", e);
}
}
// Otherwise, add new language resource
else {
resources.put(key, resource);
logger.debug("Added language: \"{}\"", key);
}
}
/**
* Adds or overlays all languages defined within the /translations
* directory of the given ServletContext. If no such language files exist,
* nothing is done. If a language is already defined, the strings from the
* will be overlaid on top of the existing strings, augmenting or
* overriding the available strings for that language. The language key
* for each language file is derived from the filename.
*
* @param context
* The ServletContext from which language files should be loaded.
*/
public void addLanguageResources(ServletContext context) {
// Get the paths of all the translation files
Set<?> resourcePaths = context.getResourcePaths(TRANSLATION_PATH);
// If no translation files found, nothing to add
if (resourcePaths == null)
return;
// Iterate through all the found language files and add them to the map
for (Object resourcePathObject : resourcePaths) {
// Each resource path is guaranteed to be a string
String resourcePath = (String) resourcePathObject;
// Parse language key from path
String languageKey = getLanguageKey(resourcePath);
if (languageKey == null) {
logger.warn("Invalid language file name: \"{}\"", resourcePath);
continue;
}
// Add/overlay new resource
addLanguageResource(
languageKey,
new WebApplicationResource(context, "application/json", resourcePath)
);
}
}
/**
* Returns a set of all unique language keys currently associated with
* language resources stored in this service. The returned set cannot be
* modified.
*
* @return
* A set of all unique language keys currently associated with this
* service.
*/
public Set<String> getLanguageKeys() {
return Collections.unmodifiableSet(resources.keySet());
}
/**
* Returns a map of all languages currently associated with this service,
* where the key of each map entry is the language key. The returned map
* cannot be modified.
*
* @return
* A map of all languages currently associated with this service.
*/
public Map<String, Resource> getLanguageResources() {
return Collections.unmodifiableMap(resources);
}
/**
* Returns a mapping of all language keys to their corresponding human-
* readable language names. If an error occurs while parsing a language
* resource, its key/name pair will simply be omitted. The returned map
* cannot be modified.
*
* @return
* A map of all language keys and their corresponding human-readable
* names.
*/
public Map<String, String> getLanguageNames() {
Map<String, String> languageNames = new HashMap<String, String>();
// For each language key/resource pair
for (Map.Entry<String, Resource> entry : resources.entrySet()) {
// Get language key and resource
String languageKey = entry.getKey();
Resource resource = entry.getValue();
// Get stream for resource
InputStream resourceStream = resource.asStream();
if (resourceStream == null) {
logger.warn("Expected language resource does not exist: \"{}\".", languageKey);
continue;
}
// Get name node of language
try {
JsonNode tree = mapper.readTree(resourceStream);
JsonNode nameNode = tree.get(LANGUAGE_DISPLAY_NAME_KEY);
// Attempt to read language name from node
String languageName;
if (nameNode == null || (languageName = nameNode.getTextValue()) == null) {
logger.warn("Root-level \"" + LANGUAGE_DISPLAY_NAME_KEY + "\" string missing or invalid in language \"{}\"", languageKey);
languageName = languageKey;
}
// Add language key/name pair to map
languageNames.put(languageKey, languageName);
}
// 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);
}
}
return Collections.unmodifiableMap(languageNames);
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2016 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.extension;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.guacamole.net.basic.resource.Resource;
/**
* Service which provides access to all HTML patches as resources, and allows
* other patch resources to be added.
*
* @author Michael Jumper
*/
public class PatchResourceService {
/**
* A list of all HTML patch resources currently defined, in the order they
* should be applied.
*/
private final List<Resource> resources = new ArrayList<Resource>();
/**
* Adds the given HTML patch resource such that it will apply to the
* Guacamole UI. The patch will be applied by the JavaScript side of the
* web application in the order that addPatchResource() is invoked.
*
* @param resource
* The HTML patch resource to add. This resource must have the mimetype
* "text/html".
*/
public void addPatchResource(Resource resource) {
resources.add(resource);
}
/**
* Adds the given HTML patch resources such that they will apply to the
* Guacamole UI. The patches will be applied by the JavaScript side of the
* web application in the order provided.
*
* @param resources
* The HTML patch resources to add. Each resource must have the
* mimetype "text/html".
*/
public void addPatchResources(Collection<Resource> resources) {
for (Resource resource : resources)
addPatchResource(resource);
}
/**
* Returns a list of all HTML patches currently associated with this
* service, in the order they should be applied. The returned list cannot
* be modified.
*
* @return
* A list of all HTML patches currently associated with this service.
*/
public List<Resource> getPatchResources() {
return Collections.unmodifiableList(resources);
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes which represent and facilitate the loading of extensions to the
* Guacamole web application.
*/
package org.apache.guacamole.net.basic.extension;
@@ -0,0 +1,99 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.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 org.apache.guacamole.environment.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Initializes the logging subsystem.
*
* @author Michael Jumper
*/
public class LogModule extends AbstractModule {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(LogModule.class);
/**
* The Guacamole server environment.
*/
private final Environment environment;
/**
* Creates a new LogModule which uses the given environment to determine
* the logging configuration.
*
* @param environment
* The environment to use when configuring logging.
*/
public LogModule(Environment environment) {
this.environment = environment;
}
@Override
protected void configure() {
// Only load logback configuration if GUACAMOLE_HOME exists
File guacamoleHome = environment.getGuacamoleHome();
if (!guacamoleHome.isDirectory())
return;
// Check for custom logback.xml
File logbackConfiguration = new File(guacamoleHome, "logback.xml");
if (!logbackConfiguration.exists())
return;
logger.info("Loading logback configuration from \"{}\".", logbackConfiguration);
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.reset();
try {
// Initialize logback
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
configurator.doConfigure(logbackConfiguration);
// Dump any errors that occur during logback init
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
}
catch (JoranException e) {
logger.error("Initialization of logback failed: {}", e.getMessage());
logger.debug("Unable to load logback configuration..", e);
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes specific to the general-purpose web application implemented by
* the Guacamole project using the Guacamole APIs.
*/
package org.apache.guacamole.net.basic;
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.properties;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty whose value is the name of a class to use to
* 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
*/
@Deprecated
public abstract class AuthenticationProviderProperty implements GuacamoleProperty<Class<AuthenticationProvider>> {
@Override
@SuppressWarnings("unchecked") // Explicitly checked within by isAssignableFrom()
public Class<AuthenticationProvider> parseValue(String authProviderClassName) throws GuacamoleException {
// If no property provided, return null.
if (authProviderClassName == null)
return null;
// Get auth provider instance
try {
// Get authentication provider class
Class<?> authProviderClass = org.apache.guacamole.net.basic.GuacamoleClassLoader.getInstance().loadClass(authProviderClassName);
// 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.");
// Return located class
return (Class<AuthenticationProvider>) authProviderClass;
}
catch (ClassNotFoundException e) {
throw new GuacamoleException("Authentication provider class not found", e);
}
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.properties;
import org.apache.guacamole.properties.FileGuacamoleProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
/**
* Properties used by the default Guacamole web application.
*
* @author Michael Jumper
*/
public class BasicGuacamoleProperties {
/**
* This class should not be instantiated.
*/
private BasicGuacamoleProperties() {}
/**
* The authentication provider to user when retrieving the authorized
* 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() {
@Override
public String getName() { return "auth-provider"; }
};
/**
* 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() {
@Override
public String getName() { return "lib-directory"; }
};
/**
* The session timeout for the API, in minutes.
*/
public static final IntegerGuacamoleProperty API_SESSION_TIMEOUT = new IntegerGuacamoleProperty() {
@Override
public String getName() { return "api-session-timeout"; }
};
/**
* Comma-separated list of all allowed languages, where each language is
* represented by a language key, such as "en" or "en_US". If specified,
* only languages within this list will be listed as available by the REST
* service.
*/
public static final StringSetProperty ALLOWED_LANGUAGES = new StringSetProperty() {
@Override
public String getName() { return "allowed-languages"; }
};
}
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.properties;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty whose value is a Set of unique Strings. The string value
* parsed to produce this set is a comma-delimited list. Duplicate values are
* ignored, as is any whitespace following delimiters. To maintain
* compatibility with the behavior of Java properties in general, only
* whitespace at the beginning of each value is ignored; trailing whitespace
* becomes part of the value.
*
* @author Michael Jumper
*/
public abstract class StringSetProperty implements GuacamoleProperty<Set<String>> {
/**
* A pattern which matches against the delimiters between values. This is
* currently simply a comma and any following whitespace. Parts of the
* input string which match this pattern will not be included in the parsed
* result.
*/
private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*");
@Override
public Set<String> parseValue(String values) throws GuacamoleException {
// If no property provided, return null.
if (values == null)
return null;
// Split string into a set of individual values
List<String> valueList = Arrays.asList(DELIMITER_PATTERN.split(values));
return new HashSet<String>(valueList);
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the properties which the Guacamole web application
* (and stock parts of it) read from guacamole.properties.
*/
package org.apache.guacamole.net.basic.properties;
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.resource;
/**
* Base abstract resource implementation which provides an associated mimetype,
* and modification time. Classes which extend AbstractResource must provide
* their own InputStream, however.
*
* @author Michael Jumper
*/
public abstract class AbstractResource implements Resource {
/**
* The mimetype of this resource.
*/
private final String mimetype;
/**
* The time this resource was last modified, in milliseconds since midnight
* of January 1, 1970 UTC.
*/
private final long lastModified;
/**
* Initializes this AbstractResource with the given mimetype and
* modification time.
*
* @param mimetype
* The mimetype of this resource.
*
* @param lastModified
* The time this resource was last modified, in milliseconds since
* midnight of January 1, 1970 UTC.
*/
public AbstractResource(String mimetype, long lastModified) {
this.mimetype = mimetype;
this.lastModified = lastModified;
}
/**
* Initializes this AbstractResource with the given mimetype. The
* modification time of the resource is set to the current system time.
*
* @param mimetype
* The mimetype of this resource.
*/
public AbstractResource(String mimetype) {
this(mimetype, System.currentTimeMillis());
}
@Override
public long getLastModified() {
return lastModified;
}
@Override
public String getMimeType() {
return mimetype;
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.resource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* A resource which contains a defined byte array.
*
* @author Michael Jumper
*/
public class ByteArrayResource extends AbstractResource {
/**
* The bytes contained by this resource.
*/
private final byte[] bytes;
/**
* Creates a new ByteArrayResource which provides access to the given byte
* array. Changes to the given byte array will affect this resource even
* after the resource is created. Changing the byte array while an input
* stream from this resource is in use has undefined behavior.
*
* @param mimetype
* The mimetype of the resource.
*
* @param bytes
* The bytes that this resource should contain.
*/
public ByteArrayResource(String mimetype, byte[] bytes) {
super(mimetype);
this.bytes = bytes;
}
@Override
public InputStream asStream() {
return new ByteArrayInputStream(bytes);
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.resource;
import java.io.InputStream;
/**
* A resource which is located within the classpath of an arbitrary
* ClassLoader.
*
* @author Michael Jumper
*/
public class ClassPathResource extends AbstractResource {
/**
* The classloader to use when reading this resource.
*/
private final ClassLoader classLoader;
/**
* The path of this resource relative to the classloader.
*/
private final String path;
/**
* Creates a new ClassPathResource which uses the given ClassLoader to
* read the resource having the given path.
*
* @param classLoader
* The ClassLoader to use when reading the resource.
*
* @param mimetype
* The mimetype of the resource.
*
* @param path
* The path of the resource relative to the given ClassLoader.
*/
public ClassPathResource(ClassLoader classLoader, String mimetype, String path) {
super(mimetype);
this.classLoader = classLoader;
this.path = path;
}
/**
* Creates a new ClassPathResource which uses the ClassLoader associated
* with the ClassPathResource class to read the resource having the given
* path.
*
* @param mimetype
* The mimetype of the resource.
*
* @param path
* The path of the resource relative to the ClassLoader associated
* with the ClassPathResource class.
*/
public ClassPathResource(String mimetype, String path) {
this(ClassPathResource.class.getClassLoader(), mimetype, path);
}
@Override
public InputStream asStream() {
return classLoader.getResourceAsStream(path);
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.resource;
import java.io.InputStream;
/**
* An arbitrary resource that can be served to a user via HTTP. Resources are
* anonymous but have a defined mimetype and corresponding input stream.
*
* @author Michael Jumper
*/
public interface Resource {
/**
* Returns the mimetype of this resource. This function MUST always return
* a value. If the type is unknown, return "application/octet-stream".
*
* @return
* The mimetype of this resource.
*/
String getMimeType();
/**
* Returns the time the resource was last modified in milliseconds since
* midnight of January 1, 1970 UTC.
*
* @return
* The time the resource was last modified, in milliseconds.
*/
long getLastModified();
/**
* Returns an InputStream which reads the contents of this resource,
* starting with the first byte. Reading from the returned InputStream will
* not affect reads from other InputStreams returned by other calls to
* asStream(). The returned InputStream must be manually closed when no
* longer needed. If the resource is unexpectedly unavailable, this will
* return null.
*
* @return
* An InputStream which reads the contents of this resource, starting
* with the first byte, or null if the resource is unavailable.
*/
InputStream asStream();
}
@@ -0,0 +1,126 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Servlet which serves a given resource for all HTTP GET requests. The HEAD
* method is correctly supported, and HTTP 304 ("Not Modified") responses will
* be properly returned for GET requests depending on the last time the
* resource was modified.
*
* @author Michael Jumper
*/
public class ResourceServlet extends HttpServlet {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ResourceServlet.class);
/**
* The size of the buffer to use when transferring data from the input
* stream of a resource to the output stream of a request.
*/
private static final int BUFFER_SIZE = 10240;
/**
* The resource to serve for every GET request.
*/
private final Resource resource;
/**
* Creates a new ResourceServlet which serves the given Resource for all
* HTTP GET requests.
*
* @param resource
* The Resource to serve.
*/
public ResourceServlet(Resource resource) {
this.resource = resource;
}
@Override
protected void doHead(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Set last modified and content type headers
response.addDateHeader("Last-Modified", resource.getLastModified());
response.setContentType(resource.getMimeType());
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Get input stream from resource
InputStream input = resource.asStream();
// If resource does not exist, return not found
if (input == null) {
logger.debug("Resource does not exist: \"{}\"", request.getServletPath());
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
try {
// Write headers
doHead(request, response);
// If not modified since "If-Modified-Since" header, return not modified
long ifModifiedSince = request.getDateHeader("If-Modified-Since");
if (resource.getLastModified() - ifModifiedSince < 1000) {
logger.debug("Resource not modified: \"{}\"", request.getServletPath());
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
int length;
byte[] buffer = new byte[BUFFER_SIZE];
// Write resource to response body
OutputStream output = response.getOutputStream();
while ((length = input.read(buffer)) != -1)
output.write(buffer, 0, length);
}
// Ensure input stream is always closed
finally {
input.close();
}
}
}
@@ -0,0 +1,153 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.resource;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
/**
* A resource which is the logical concatenation of other resources.
*
* @author Michael Jumper
*/
public class SequenceResource extends AbstractResource {
/**
* The resources to be concatenated.
*/
private final Iterable<Resource> resources;
/**
* Returns the mimetype of the first resource in the given Iterable, or
* "application/octet-stream" if no resources are provided.
*
* @param resources
* The resources from which the mimetype should be retrieved.
*
* @return
* The mimetype of the first resource, or "application/octet-stream"
* if no resources were provided.
*/
private static String getMimeType(Iterable<Resource> resources) {
// If no resources, just assume application/octet-stream
Iterator<Resource> resourceIterator = resources.iterator();
if (!resourceIterator.hasNext())
return "application/octet-stream";
// Return mimetype of first resource
return resourceIterator.next().getMimeType();
}
/**
* Creates a new SequenceResource as the logical concatenation of the
* given resources. Each resource is concatenated in iteration order as
* needed when reading from the input stream of the SequenceResource.
*
* @param mimetype
* The mimetype of the resource.
*
* @param resources
* The resources to concatenate within the InputStream of this
* SequenceResource.
*/
public SequenceResource(String mimetype, Iterable<Resource> resources) {
super(mimetype);
this.resources = resources;
}
/**
* Creates a new SequenceResource as the logical concatenation of the
* given resources. Each resource is concatenated in iteration order as
* needed when reading from the input stream of the SequenceResource. The
* mimetype of the resulting concatenation is derived from the first
* resource.
*
* @param resources
* The resources to concatenate within the InputStream of this
* SequenceResource.
*/
public SequenceResource(Iterable<Resource> resources) {
super(getMimeType(resources));
this.resources = resources;
}
/**
* Creates a new SequenceResource as the logical concatenation of the
* given resources. Each resource is concatenated in iteration order as
* needed when reading from the input stream of the SequenceResource.
*
* @param mimetype
* The mimetype of the resource.
*
* @param resources
* The resources to concatenate within the InputStream of this
* SequenceResource.
*/
public SequenceResource(String mimetype, Resource... resources) {
this(mimetype, Arrays.asList(resources));
}
/**
* Creates a new SequenceResource as the logical concatenation of the
* given resources. Each resource is concatenated in iteration order as
* needed when reading from the input stream of the SequenceResource. The
* mimetype of the resulting concatenation is derived from the first
* resource.
*
* @param resources
* The resources to concatenate within the InputStream of this
* SequenceResource.
*/
public SequenceResource(Resource... resources) {
this(Arrays.asList(resources));
}
@Override
public InputStream asStream() {
return new SequenceInputStream(new Enumeration<InputStream>() {
/**
* Iterator over all resources associated with this
* SequenceResource.
*/
private final Iterator<Resource> resourceIterator = resources.iterator();
@Override
public boolean hasMoreElements() {
return resourceIterator.hasNext();
}
@Override
public InputStream nextElement() {
return resourceIterator.next().asStream();
}
});
}
}
@@ -0,0 +1,116 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.resource;
import java.io.InputStream;
import javax.servlet.ServletContext;
/**
* A resource which is located within the classpath associated with another
* class.
*
* @author Michael Jumper
*/
public class WebApplicationResource extends AbstractResource {
/**
* The servlet context to use when reading the resource and, if necessary,
* when determining the mimetype of the resource.
*/
private final ServletContext context;
/**
* The path of this resource relative to the ServletContext.
*/
private final String path;
/**
* Derives a mimetype from the filename within the given path using the
* given ServletContext, if possible.
*
* @param context
* The ServletContext to use to derive the mimetype.
*
* @param path
* The path to derive the mimetype from.
*
* @return
* An appropriate mimetype based on the name of the file in the path,
* or "application/octet-stream" if no mimetype could be determined.
*/
private static String getMimeType(ServletContext context, String path) {
// If mimetype is known, use defined mimetype
String mimetype = context.getMimeType(path);
if (mimetype != null)
return mimetype;
// Otherwise, default to application/octet-stream
return "application/octet-stream";
}
/**
* Creates a new WebApplicationResource which serves the resource at the
* given path relative to the given ServletContext. Rather than deriving
* the mimetype of the resource from the filename within the path, the
* mimetype given is used.
*
* @param context
* The ServletContext to use when reading the resource.
*
* @param mimetype
* The mimetype of the resource.
*
* @param path
* The path of the resource relative to the given ServletContext.
*/
public WebApplicationResource(ServletContext context, String mimetype, String path) {
super(mimetype);
this.context = context;
this.path = path;
}
/**
* Creates a new WebApplicationResource which serves the resource at the
* given path relative to the given ServletContext. The mimetype of the
* resource is automatically determined based on the filename within the
* path.
*
* @param context
* The ServletContext to use when reading the resource and deriving the
* mimetype.
*
* @param path
* The path of the resource relative to the given ServletContext.
*/
public WebApplicationResource(ServletContext context, String path) {
this(context, getMimeType(context, path), path);
}
@Override
public InputStream asStream() {
return context.getResourceAsStream(path);
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes which describe and provide access to arbitrary resources, such as
* the contents of the classpath of a classloader, or files within the web
* application itself.
*/
package org.apache.guacamole.net.basic.resource;
@@ -0,0 +1,183 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
import java.util.Collection;
import javax.ws.rs.core.Response;
import org.apache.guacamole.form.Field;
/**
* Describes an error that occurred within a REST endpoint.
*
* @author James Muehlner
* @author Michael Jumper
*/
public class APIError {
/**
* The error message.
*/
private final String message;
/**
* All expected request parameters, if any, as a collection of fields.
*/
private final Collection<Field> expected;
/**
* The type of error that occurred.
*/
private final Type type;
/**
* All possible types of REST API errors.
*/
public enum Type {
/**
* The requested operation could not be performed because the request
* itself was malformed.
*/
BAD_REQUEST(Response.Status.BAD_REQUEST),
/**
* The credentials provided were invalid.
*/
INVALID_CREDENTIALS(Response.Status.FORBIDDEN),
/**
* The credentials provided were not necessarily invalid, but were not
* sufficient to determine validity.
*/
INSUFFICIENT_CREDENTIALS(Response.Status.FORBIDDEN),
/**
* An internal server error has occurred.
*/
INTERNAL_ERROR(Response.Status.INTERNAL_SERVER_ERROR),
/**
* An object related to the request does not exist.
*/
NOT_FOUND(Response.Status.NOT_FOUND),
/**
* Permission was denied to perform the requested operation.
*/
PERMISSION_DENIED(Response.Status.FORBIDDEN);
/**
* The HTTP status associated with this error type.
*/
private final Response.Status status;
/**
* Defines a new error type associated with the given HTTP status.
*
* @param status
* The HTTP status to associate with the error type.
*/
Type(Response.Status status) {
this.status = status;
}
/**
* Returns the HTTP status associated with this error type.
*
* @return
* The HTTP status associated with this error type.
*/
public Response.Status getStatus() {
return status;
}
}
/**
* Create a new APIError with the specified error message.
*
* @param type
* The type of error that occurred.
*
* @param message
* The error message.
*/
public APIError(Type type, String message) {
this.type = type;
this.message = message;
this.expected = null;
}
/**
* Create a new APIError with the specified error message and parameter
* information.
*
* @param type
* The type of error that occurred.
*
* @param message
* The error message.
*
* @param expected
* All parameters expected in the original request, or now required as
* a result of the original request, as a collection of fields.
*/
public APIError(Type type, String message, Collection<Field> expected) {
this.type = type;
this.message = message;
this.expected = expected;
}
/**
* Returns the type of error that occurred.
*
* @return
* The type of error that occurred.
*/
public Type getType() {
return type;
}
/**
* Returns a collection of all required parameters, where each parameter is
* represented by a field.
*
* @return
* A collection of all required parameters.
*/
public Collection<Field> getExpected() {
return expected;
}
/**
* Returns a human-readable error message describing the error that
* occurred.
*
* @return
* A human-readable error message.
*/
public String getMessage() {
return message;
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
import java.util.Collection;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import org.apache.guacamole.form.Field;
/**
* An exception that will result in the given error error information being
* returned from the API layer. All error messages have the same format which
* is defined by APIError.
*
* @author James Muehlner
* @author Michael Jumper
*/
public class APIException extends WebApplicationException {
/**
* Construct a new APIException with the given error. All information
* associated with this new exception will be extracted from the given
* APIError.
*
* @param error
* The error that occurred.
*/
public APIException(APIError error) {
super(Response.status(error.getType().getStatus()).entity(error).build());
}
/**
* Creates a new APIException with the given type and message. The
* corresponding APIError will be created from the provided information.
*
* @param type
* The type of error that occurred.
*
* @param message
* A human-readable message describing the error.
*/
public APIException(APIError.Type type, String message) {
this(new APIError(type, message));
}
/**
* Creates a new APIException with the given type, message, and parameter
* information. The corresponding APIError will be created from the
* provided information.
*
* @param type
* The type of error that occurred.
*
* @param message
* A human-readable message describing the error.
*
* @param expected
* All parameters expected in the original request, or now required as
* a result of the original request, as a collection of fields.
*/
public APIException(APIError.Type type, String message, Collection<Field> expected) {
this(new APIError(type, message, expected));
}
}
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
/**
* An object for representing the body of a HTTP PATCH method.
* See https://tools.ietf.org/html/rfc6902
*
* @author James Muehlner
* @param <T> The type of object being patched.
*/
public class APIPatch<T> {
/**
* The possible operations for a PATCH request.
*/
public enum Operation {
add, remove, test, copy, replace, move
}
/**
* The operation to perform for this patch.
*/
private Operation op;
/**
* The value for this patch.
*/
private T value;
/**
* The path for this patch.
*/
private String path;
/**
* Returns the operation for this patch.
* @return the operation for this patch.
*/
public Operation getOp() {
return op;
}
/**
* Set the operation for this patch.
* @param op The operation for this patch.
*/
public void setOp(Operation op) {
this.op = op;
}
/**
* Returns the value of this patch.
* @return The value of this patch.
*/
public T getValue() {
return value;
}
/**
* Sets the value of this patch.
* @param value The value of this patch.
*/
public void setValue(T value) {
this.value = value;
}
/**
* Returns the path for this patch.
* @return The path for this patch.
*/
public String getPath() {
return path;
}
/**
* Set the path for this patch.
* @param path The path for this patch.
*/
public void setPath(String path) {
this.path = path;
}
}
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.ws.rs.core.MultivaluedMap;
/**
* Wrapper for HttpServletRequest which uses a given MultivaluedMap to provide
* the values of all request parameters.
*
* @author Michael Jumper
*/
public class APIRequest extends HttpServletRequestWrapper {
/**
* Map of all request parameter names to their corresponding values.
*/
private final Map<String, String[]> parameters;
/**
* Wraps the given HttpServletRequest, using the given MultivaluedMap to
* provide all request parameters. All HttpServletRequest functions which
* do not deal with parameter names and values are delegated to the wrapped
* request.
*
* @param request
* The HttpServletRequest to wrap.
*
* @param parameters
* All request parameters.
*/
public APIRequest(HttpServletRequest request,
MultivaluedMap<String, String> parameters) {
super(request);
// Copy parameters from given MultivaluedMap
this.parameters = new HashMap<String, String[]>(parameters.size());
for (Map.Entry<String, List<String>> entry : parameters.entrySet()) {
// Get parameter name and all corresponding values
String name = entry.getKey();
List<String> values = entry.getValue();
// Add parameters to map
this.parameters.put(name, values.toArray(new String[values.size()]));
}
}
@Override
public String[] getParameterValues(String name) {
return parameters.get(name);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(parameters.keySet());
}
@Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(parameters);
}
@Override
public String getParameter(String name) {
// If no such parameter exists, just return null
String[] values = getParameterValues(name);
if (values == null)
return null;
// Otherwise, return first value
return values[0];
}
}
@@ -0,0 +1,280 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
import java.util.List;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup;
/**
* Provides easy access and automatic error handling for retrieval of objects,
* such as users, connections, or connection groups. REST API semantics, such
* as the special root connection group identifier, are also handled
* automatically.
*/
public class ObjectRetrievalService {
/**
* Retrieves a single UserContext from the given GuacamoleSession, which
* may contain multiple UserContexts.
*
* @param session
* The GuacamoleSession to retrieve the UserContext from.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider that created the
* UserContext being retrieved. Only one UserContext per User per
* AuthenticationProvider can exist.
*
* @return
* The UserContext that was created by the AuthenticationProvider
* having the given identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the UserContext, or if the
* UserContext does not exist.
*/
public UserContext retrieveUserContext(GuacamoleSession session,
String authProviderIdentifier) throws GuacamoleException {
// Get list of UserContexts
List<UserContext> userContexts = session.getUserContexts();
// Locate and return the UserContext associated with the
// AuthenticationProvider having the given identifier, if any
for (UserContext userContext : userContexts) {
// Get AuthenticationProvider associated with current UserContext
AuthenticationProvider authProvider = userContext.getAuthenticationProvider();
// If AuthenticationProvider identifier matches, done
if (authProvider.getIdentifier().equals(authProviderIdentifier))
return userContext;
}
throw new GuacamoleResourceNotFoundException("Session not associated with authentication provider \"" + authProviderIdentifier + "\".");
}
/**
* Retrieves a single user from the given user context.
*
* @param userContext
* The user context to retrieve the user from.
*
* @param identifier
* The identifier of the user to retrieve.
*
* @return
* The user having the given identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the user, or if the
* user does not exist.
*/
public User retrieveUser(UserContext userContext,
String identifier) throws GuacamoleException {
// Get user directory
Directory<User> directory = userContext.getUserDirectory();
// Pull specified user
User user = directory.get(identifier);
if (user == null)
throw new GuacamoleResourceNotFoundException("No such user: \"" + identifier + "\"");
return user;
}
/**
* Retrieves a single user from the given GuacamoleSession.
*
* @param session
* The GuacamoleSession to retrieve the user from.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider that created the
* UserContext from which the user should be retrieved. Only one
* UserContext per User per AuthenticationProvider can exist.
*
* @param identifier
* The identifier of the user to retrieve.
*
* @return
* The user having the given identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the user, or if the
* user does not exist.
*/
public User retrieveUser(GuacamoleSession session, String authProviderIdentifier,
String identifier) throws GuacamoleException {
UserContext userContext = retrieveUserContext(session, authProviderIdentifier);
return retrieveUser(userContext, identifier);
}
/**
* Retrieves a single connection from the given user context.
*
* @param userContext
* The user context to retrieve the connection from.
*
* @param identifier
* The identifier of the connection to retrieve.
*
* @return
* The connection having the given identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection, or if the
* connection does not exist.
*/
public Connection retrieveConnection(UserContext userContext,
String identifier) throws GuacamoleException {
// Get connection directory
Directory<Connection> directory = userContext.getConnectionDirectory();
// Pull specified connection
Connection connection = directory.get(identifier);
if (connection == null)
throw new GuacamoleResourceNotFoundException("No such connection: \"" + identifier + "\"");
return connection;
}
/**
* Retrieves a single connection from the given GuacamoleSession.
*
* @param session
* The GuacamoleSession to retrieve the connection from.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider that created the
* UserContext from which the connection should be retrieved. Only one
* UserContext per User per AuthenticationProvider can exist.
*
* @param identifier
* The identifier of the connection to retrieve.
*
* @return
* The connection having the given identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection, or if the
* connection does not exist.
*/
public Connection retrieveConnection(GuacamoleSession session,
String authProviderIdentifier, String identifier)
throws GuacamoleException {
UserContext userContext = retrieveUserContext(session, authProviderIdentifier);
return retrieveConnection(userContext, identifier);
}
/**
* Retrieves a single connection group from the given user context. If
* the given identifier the REST API root identifier, the root connection
* group will be returned. The underlying authentication provider may
* additionally use a different identifier for root.
*
* @param userContext
* The user context to retrieve the connection group from.
*
* @param identifier
* The identifier of the connection group to retrieve.
*
* @return
* The connection group having the given identifier, or the root
* connection group if the identifier the root identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection group, or if the
* connection group does not exist.
*/
public ConnectionGroup retrieveConnectionGroup(UserContext userContext,
String identifier) throws GuacamoleException {
// Use root group if identifier is the standard root identifier
if (identifier != null && identifier.equals(APIConnectionGroup.ROOT_IDENTIFIER))
return userContext.getRootConnectionGroup();
// Pull specified connection group otherwise
Directory<ConnectionGroup> directory = userContext.getConnectionGroupDirectory();
ConnectionGroup connectionGroup = directory.get(identifier);
if (connectionGroup == null)
throw new GuacamoleResourceNotFoundException("No such connection group: \"" + identifier + "\"");
return connectionGroup;
}
/**
* Retrieves a single connection group from the given GuacamoleSession. If
* the given identifier is the REST API root identifier, the root
* connection group will be returned. The underlying authentication
* provider may additionally use a different identifier for root.
*
* @param session
* The GuacamoleSession to retrieve the connection group from.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider that created the
* UserContext from which the connection group should be retrieved.
* Only one UserContext per User per AuthenticationProvider can exist.
*
* @param identifier
* The identifier of the connection group to retrieve.
*
* @return
* The connection group having the given identifier, or the root
* connection group if the identifier is the root identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection group, or if the
* connection group does not exist.
*/
public ConnectionGroup retrieveConnectionGroup(GuacamoleSession session,
String authProviderIdentifier, String identifier) throws GuacamoleException {
UserContext userContext = retrieveUserContext(session, authProviderIdentifier);
return retrieveConnectionGroup(userContext, identifier);
}
}
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.ws.rs.HttpMethod;
/**
* An annotation for using the HTTP PATCH method in the REST endpoints.
*
* @author James Muehlner
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("PATCH")
public @interface PATCH {}
@@ -0,0 +1,274 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
import com.google.inject.Inject;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import javax.ws.rs.FormParam;
import javax.ws.rs.QueryParam;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A method interceptor which wraps custom exception handling around methods
* which can throw GuacamoleExceptions and which are exposed through the REST
* interface. The various types of GuacamoleExceptions are automatically
* translated into appropriate HTTP responses, including JSON describing the
* error that occurred.
*
* @author James Muehlner
* @author Michael Jumper
*/
public class RESTExceptionWrapper implements MethodInterceptor {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(RESTExceptionWrapper.class);
/**
* Service for authenticating users and managing their Guacamole sessions.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Determines whether the given set of annotations describes an HTTP
* request parameter of the given name. For a parameter to be associated
* with an HTTP request parameter, it must be annotated with either the
* <code>@QueryParam</code> or <code>@FormParam</code> annotations.
*
* @param annotations
* The annotations associated with the Java parameter being checked.
*
* @param name
* The name of the HTTP request parameter.
*
* @return
* true if the given set of annotations describes an HTTP request
* parameter having the given name, false otherwise.
*/
private boolean isRequestParameter(Annotation[] annotations, String name) {
// Search annotations for associated HTTP parameters
for (Annotation annotation : annotations) {
// Check if parameter is associated with the HTTP query string
if (annotation instanceof QueryParam && name.equals(((QueryParam) annotation).value()))
return true;
// Failing that, check whether the parameter is associated with the
// HTTP request body
if (annotation instanceof FormParam && name.equals(((FormParam) annotation).value()))
return true;
}
// No parameter annotations are present
return false;
}
/**
* Returns the authentication token that was passed in the given method
* invocation. If the given method invocation is not associated with an
* HTTP request (it lacks the appropriate JAX-RS annotations) or there is
* no authentication token, null is returned.
*
* @param invocation
* The method invocation whose corresponding authentication token
* should be determined.
*
* @return
* The authentication token passed in the given method invocation, or
* null if there is no such token.
*/
private String getAuthenticationToken(MethodInvocation invocation) {
Method method = invocation.getMethod();
// Get the types and annotations associated with each parameter
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class<?>[] parameterTypes = method.getParameterTypes();
// The Java standards require these to be parallel arrays
assert(parameterAnnotations.length == parameterTypes.length);
// Iterate through all parameters, looking for the authentication token
for (int i = 0; i < parameterTypes.length; i++) {
// Only inspect String parameters
Class<?> parameterType = parameterTypes[i];
if (parameterType != String.class)
continue;
// Parameter must be declared as a REST service parameter
Annotation[] annotations = parameterAnnotations[i];
if (!isRequestParameter(annotations, "token"))
continue;
// The token parameter has been found - return its value
Object[] args = invocation.getArguments();
return (String) args[i];
}
// No token parameter is defined
return null;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
// Invoke wrapped method
try {
return invocation.proceed();
}
// Ensure any associated session is invalidated if unauthorized
catch (GuacamoleUnauthorizedException e) {
// Pull authentication token from request
String token = getAuthenticationToken(invocation);
// If there is an associated auth token, invalidate it
if (authenticationService.destroyGuacamoleSession(token))
logger.debug("Implicitly invalidated session for token \"{}\".", token);
// Continue with exception processing
throw e;
}
}
// Additional credentials are needed
catch (GuacamoleInsufficientCredentialsException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Permission denied.";
throw new APIException(
APIError.Type.INSUFFICIENT_CREDENTIALS,
message,
e.getCredentialsInfo().getFields()
);
}
// The provided credentials are wrong
catch (GuacamoleInvalidCredentialsException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Permission denied.";
throw new APIException(
APIError.Type.INVALID_CREDENTIALS,
message,
e.getCredentialsInfo().getFields()
);
}
// Generic permission denied
catch (GuacamoleSecurityException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Permission denied.";
throw new APIException(
APIError.Type.PERMISSION_DENIED,
message
);
}
// Arbitrary resource not found
catch (GuacamoleResourceNotFoundException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Not found.";
throw new APIException(
APIError.Type.NOT_FOUND,
message
);
}
// Arbitrary bad requests
catch (GuacamoleClientException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Invalid request.";
throw new APIException(
APIError.Type.BAD_REQUEST,
message
);
}
// All other errors
catch (GuacamoleException e) {
// Generate default message
String message = e.getMessage();
if (message == null)
message = "Unexpected server error.";
// Ensure internal errors are logged at the debug level
logger.debug("Unexpected exception in REST endpoint.", e);
throw new APIException(
APIError.Type.INTERNAL_ERROR,
message
);
}
}
}
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
import com.google.inject.matcher.AbstractMatcher;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import javax.ws.rs.HttpMethod;
import org.apache.guacamole.GuacamoleException;
/**
* A Guice Matcher which matches only methods which throw GuacamoleException
* (or a subclass thereof) and are explicitly annotated as with an HTTP method
* annotation like <code>@GET</code> or <code>@POST</code>. Any method which
* throws GuacamoleException and is annotated with an annotation that is
* annotated with <code>@HttpMethod</code> will match.
*
* @author Michael Jumper
*/
public class RESTMethodMatcher extends AbstractMatcher<Method> {
/**
* Returns whether the given method throws the specified exception type,
* including any subclasses of that type.
*
* @param method
* The method to test.
*
* @param exceptionType
* The exception type to test for.
*
* @return
* true if the given method throws an exception of the specified type,
* false otherwise.
*/
private boolean methodThrowsException(Method method,
Class<? extends Exception> exceptionType) {
// Check whether the method throws an exception of the specified type
for (Class<?> thrownType : method.getExceptionTypes()) {
if (exceptionType.isAssignableFrom(thrownType))
return true;
}
// No such exception is declared to be thrown
return false;
}
/**
* Returns whether the given method is annotated as a REST method. A REST
* method is annotated with an annotation which is annotated with
* <code>@HttpMethod</code>.
*
* @param method
* The method to test.
*
* @return
* true if the given method is annotated as a REST method, false
* otherwise.
*/
private boolean isRESTMethod(Method method) {
// Check whether the required REST annotations are present
for (Annotation annotation : method.getAnnotations()) {
// A method is a REST method if it is annotated with @HttpMethod
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType.isAnnotationPresent(HttpMethod.class))
return true;
}
// The method is not an HTTP method
return false;
}
@Override
public boolean matches(Method method) {
// Guacamole REST methods are REST methods which throw
// GuacamoleExceptions
return isRESTMethod(method)
&& methodThrowsException(method, GuacamoleException.class);
}
}
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest;
import com.google.inject.Scopes;
import com.google.inject.matcher.Matchers;
import com.google.inject.servlet.ServletModule;
import com.sun.jersey.guice.spi.container.servlet.GuiceContainer;
import org.aopalliance.intercept.MethodInterceptor;
import org.codehaus.jackson.jaxrs.JacksonJsonProvider;
import org.apache.guacamole.net.basic.rest.auth.TokenRESTService;
import org.apache.guacamole.net.basic.rest.connection.ConnectionRESTService;
import org.apache.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService;
import org.apache.guacamole.net.basic.rest.activeconnection.ActiveConnectionRESTService;
import org.apache.guacamole.net.basic.rest.auth.AuthTokenGenerator;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.apache.guacamole.net.basic.rest.auth.SecureRandomAuthTokenGenerator;
import org.apache.guacamole.net.basic.rest.auth.TokenSessionMap;
import org.apache.guacamole.net.basic.rest.history.HistoryRESTService;
import org.apache.guacamole.net.basic.rest.language.LanguageRESTService;
import org.apache.guacamole.net.basic.rest.patch.PatchRESTService;
import org.apache.guacamole.net.basic.rest.schema.SchemaRESTService;
import org.apache.guacamole.net.basic.rest.user.UserRESTService;
/**
* A Guice Module to set up the servlet mappings and authentication-specific
* dependency injection for the Guacamole REST API.
*
* @author James Muehlner
* @author Michael Jumper
*/
public class RESTServiceModule extends ServletModule {
/**
* Singleton instance of TokenSessionMap.
*/
private final TokenSessionMap tokenSessionMap;
/**
* Creates a module which handles binding of REST services and related
* authentication objects, including the singleton TokenSessionMap.
*
* @param tokenSessionMap
* An instance of TokenSessionMap to inject as a singleton wherever
* needed.
*/
public RESTServiceModule(TokenSessionMap tokenSessionMap) {
this.tokenSessionMap = tokenSessionMap;
}
@Override
protected void configureServlets() {
// Bind session map
bind(TokenSessionMap.class).toInstance(tokenSessionMap);
// Bind low-level services
bind(AuthenticationService.class);
bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class);
// Automatically translate GuacamoleExceptions for REST methods
MethodInterceptor interceptor = new RESTExceptionWrapper();
requestInjection(interceptor);
bindInterceptor(Matchers.any(), new RESTMethodMatcher(), interceptor);
// Bind convenience services used by the REST API
bind(ObjectRetrievalService.class);
// Set up the API endpoints
bind(ActiveConnectionRESTService.class);
bind(ConnectionGroupRESTService.class);
bind(ConnectionRESTService.class);
bind(HistoryRESTService.class);
bind(LanguageRESTService.class);
bind(PatchRESTService.class);
bind(SchemaRESTService.class);
bind(TokenRESTService.class);
bind(UserRESTService.class);
// Set up the servlet and JSON mappings
bind(GuiceContainer.class);
bind(JacksonJsonProvider.class).in(Scopes.SINGLETON);
serve("/api/*").with(GuiceContainer.class);
}
}
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.activeconnection;
import java.util.Date;
import org.apache.guacamole.net.auth.ActiveConnection;
/**
* Information related to active connections which may be exposed through the
* REST endpoints.
*
* @author Michael Jumper
*/
public class APIActiveConnection {
/**
* The identifier of the active connection itself.
*/
private final String identifier;
/**
* The identifier of the connection associated with this
* active connection.
*/
private final String connectionIdentifier;
/**
* The date and time the connection began.
*/
private final Date startDate;
/**
* The host from which the connection originated, if known.
*/
private final String remoteHost;
/**
* The name of the user who used or is using the connection.
*/
private final String username;
/**
* Creates a new APIActiveConnection, copying the information from the given
* active connection.
*
* @param connection
* The active connection to copy data from.
*/
public APIActiveConnection(ActiveConnection connection) {
this.identifier = connection.getIdentifier();
this.connectionIdentifier = connection.getConnectionIdentifier();
this.startDate = connection.getStartDate();
this.remoteHost = connection.getRemoteHost();
this.username = connection.getUsername();
}
/**
* Returns the identifier of the connection associated with this tunnel.
*
* @return
* The identifier of the connection associated with this tunnel.
*/
public String getConnectionIdentifier() {
return connectionIdentifier;
}
/**
* Returns the date and time the connection began.
*
* @return
* The date and time the connection began.
*/
public Date getStartDate() {
return startDate;
}
/**
* Returns the remote host from which this connection originated.
*
* @return
* The remote host from which this connection originated.
*/
public String getRemoteHost() {
return remoteHost;
}
/**
* Returns the name of the user who used or is using the connection at the
* times given by this tunnel.
*
* @return
* The name of the user who used or is using the associated connection.
*/
public String getUsername() {
return username;
}
/**
* Returns the identifier of the active connection itself. This is
* distinct from the connection identifier, and uniquely identifies a
* specific use of a connection.
*
* @return
* The identifier of the active connection.
*/
public String getIdentifier() {
return identifier;
}
}
@@ -0,0 +1,196 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.activeconnection;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleUnsupportedException;
import org.apache.guacamole.net.auth.ActiveConnection;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.rest.APIPatch;
import org.apache.guacamole.net.basic.rest.ObjectRetrievalService;
import org.apache.guacamole.net.basic.rest.PATCH;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A REST Service for retrieving and managing the tunnels of active connections.
*
* @author Michael Jumper
*/
@Path("/data/{dataSource}/activeConnections")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ActiveConnectionRESTService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ActiveConnectionRESTService.class);
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/**
* Gets a list of active connections in the system, filtering the returned
* list by the given permissions, if specified.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the active connections to be retrieved.
*
* @param permissions
* The set of permissions to filter with. A user must have one or more
* of these permissions for a user to appear in the result.
* If null, no filtering will be performed.
*
* @return
* A list of all active connections. If a permission was specified,
* this list will contain only those active connections for which the
* current user has that permission.
*
* @throws GuacamoleException
* If an error is encountered while retrieving active connections.
*/
@GET
public Map<String, APIActiveConnection> getActiveConnections(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@QueryParam("permission") List<ObjectPermission.Type> permissions)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
User self = userContext.self();
// Do not filter on permissions if no permissions are specified
if (permissions != null && permissions.isEmpty())
permissions = null;
// An admin user has access to any connection
SystemPermissionSet systemPermissions = self.getSystemPermissions();
boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER);
// Get the directory
Directory<ActiveConnection> activeConnectionDirectory = userContext.getActiveConnectionDirectory();
// Filter connections, if requested
Collection<String> activeConnectionIdentifiers = activeConnectionDirectory.getIdentifiers();
if (!isAdmin && permissions != null) {
ObjectPermissionSet activeConnectionPermissions = self.getActiveConnectionPermissions();
activeConnectionIdentifiers = activeConnectionPermissions.getAccessibleObjects(permissions, activeConnectionIdentifiers);
}
// Retrieve all active connections , converting to API active connections
Map<String, APIActiveConnection> apiActiveConnections = new HashMap<String, APIActiveConnection>();
for (ActiveConnection activeConnection : activeConnectionDirectory.getAll(activeConnectionIdentifiers))
apiActiveConnections.put(activeConnection.getIdentifier(), new APIActiveConnection(activeConnection));
return apiActiveConnections;
}
/**
* Applies the given active connection patches. This operation currently
* only supports deletion of active connections through the "remove" patch
* operation. Deleting an active connection effectively kills the
* connection. The path of each patch operation is of the form "/ID"
* where ID is the identifier of the active connection being modified.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the active connections to be deleted.
*
* @param patches
* The active connection patches to apply for this request.
*
* @throws GuacamoleException
* If an error occurs while deleting the active connections.
*/
@PATCH
public void patchTunnels(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
List<APIPatch<String>> patches) throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the directory
Directory<ActiveConnection> activeConnectionDirectory = userContext.getActiveConnectionDirectory();
// Close each connection listed for removal
for (APIPatch<String> patch : patches) {
// Only remove is supported
if (patch.getOp() != APIPatch.Operation.remove)
throw new GuacamoleUnsupportedException("Only the \"remove\" operation is supported when patching active connections.");
// Retrieve and validate path
String path = patch.getPath();
if (!path.startsWith("/"))
throw new GuacamoleClientException("Patch paths must start with \"/\".");
// Close connection
activeConnectionDirectory.remove(path.substring(1));
}
}
}
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.auth;
/**
* A simple object to represent an auth token/username pair in the API.
*
* @author James Muehlner
*/
public class APIAuthenticationResponse {
/**
* The auth token.
*/
private final String authToken;
/**
* The username of the user that authenticated.
*/
private final String username;
/**
* The unique identifier of the data source from which this user account
* came. Although this user account may exist across several data sources
* (AuthenticationProviders), this will be the unique identifier of the
* AuthenticationProvider that authenticated this user for the current
* session.
*/
private final String dataSource;
/**
* Returns the unique authentication token which identifies the current
* session.
*
* @return
* The user's authentication token.
*/
public String getAuthToken() {
return authToken;
}
/**
* Returns the user identified by the authentication token associated with
* the current session.
*
* @return
* The user identified by this authentication token.
*/
public String getUsername() {
return username;
}
/**
* Returns the unique identifier of the data source associated with the user
* account associated with this auth token.
*
* @return
* The unique identifier of the data source associated with the user
* account associated with this auth token.
*/
public String getDataSource() {
return dataSource;
}
/**
* Create a new APIAuthToken Object with the given auth token.
*
* @param dataSource
* The unique identifier of the AuthenticationProvider which
* authenticated the user.
*
* @param authToken
* The auth token to create the new APIAuthToken with.
*
* @param username
* The username of the user owning the given token.
*/
public APIAuthenticationResponse(String dataSource, String authToken, String username) {
this.dataSource = dataSource;
this.authToken = authToken;
this.username = username;
}
}
@@ -0,0 +1,133 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.auth;
import java.util.Collections;
import java.util.List;
/**
* A simple object which describes the result of an authentication operation,
* including the resulting token.
*
* @author James Muehlner
* @author Michael Jumper
*/
public class APIAuthenticationResult {
/**
* The unique token generated for the user that authenticated.
*/
private final String authToken;
/**
* The username of the user that authenticated.
*/
private final String username;
/**
* The unique identifier of the data source from which this user account
* came. Although this user account may exist across several data sources
* (AuthenticationProviders), this will be the unique identifier of the
* AuthenticationProvider that authenticated this user for the current
* session.
*/
private final String dataSource;
/**
* The identifiers of all data sources available to this user.
*/
private final List<String> availableDataSources;
/**
* Returns the unique authentication token which identifies the current
* session.
*
* @return
* The user's authentication token.
*/
public String getAuthToken() {
return authToken;
}
/**
* Returns the user identified by the authentication token associated with
* the current session.
*
* @return
* The user identified by this authentication token.
*/
public String getUsername() {
return username;
}
/**
* Returns the unique identifier of the data source associated with the user
* account associated with the current session.
*
* @return
* The unique identifier of the data source associated with the user
* account associated with the current session.
*/
public String getDataSource() {
return dataSource;
}
/**
* Returns the identifiers of all data sources available to the user
* associated with the current session.
*
* @return
* The identifiers of all data sources available to the user associated
* with the current session.
*/
public List<String> getAvailableDataSources() {
return availableDataSources;
}
/**
* Create a new APIAuthenticationResult object containing the given data.
*
* @param authToken
* The unique token generated for the user that authenticated, to be
* used for the duration of their session.
*
* @param username
* The username of the user owning the given token.
*
* @param dataSource
* The unique identifier of the AuthenticationProvider which
* authenticated the user.
*
* @param availableDataSources
* The unique identifier of all AuthenticationProviders to which the
* user now has access.
*/
public APIAuthenticationResult(String authToken, String username,
String dataSource, List<String> availableDataSources) {
this.authToken = authToken;
this.username = username;
this.dataSource = dataSource;
this.availableDataSources = Collections.unmodifiableList(availableDataSources);
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.auth;
/**
* Generates an auth token for an authenticated user.
*
* @author James Muehlner
*/
public interface AuthTokenGenerator {
/**
* Get a new auth token.
*
* @return A new auth token.
*/
public String getToken();
}
@@ -0,0 +1,475 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.auth;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A service for performing authentication checks in REST endpoints.
*
* @author James Muehlner
* @author Michael Jumper
*/
public class AuthenticationService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(AuthenticationService.class);
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* All configured authentication providers which can be used to
* authenticate users or retrieve data associated with authenticated users.
*/
@Inject
private List<AuthenticationProvider> authProviders;
/**
* The map of auth tokens to sessions for the REST endpoints.
*/
@Inject
private TokenSessionMap tokenSessionMap;
/**
* A generator for creating new auth tokens.
*/
@Inject
private AuthTokenGenerator authTokenGenerator;
/**
* Regular expression which matches any IPv4 address.
*/
private static final String IPV4_ADDRESS_REGEX = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})";
/**
* Regular expression which matches any IPv6 address.
*/
private static final String IPV6_ADDRESS_REGEX = "([0-9a-fA-F]*(:[0-9a-fA-F]*){0,7})";
/**
* Regular expression which matches any IP address, regardless of version.
*/
private static final String IP_ADDRESS_REGEX = "(" + IPV4_ADDRESS_REGEX + "|" + IPV6_ADDRESS_REGEX + ")";
/**
* Pattern which matches valid values of the de-facto standard
* "X-Forwarded-For" header.
*/
private static final Pattern X_FORWARDED_FOR = Pattern.compile("^" + IP_ADDRESS_REGEX + "(, " + IP_ADDRESS_REGEX + ")*$");
/**
* Returns a formatted string containing an IP address, or list of IP
* addresses, which represent the HTTP client and any involved proxies. As
* the headers used to determine proxies can easily be forged, this data is
* superficially validated to ensure that it at least looks like a list of
* IPs.
*
* @param request
* The HTTP request to format.
*
* @return
* A formatted string containing one or more IP addresses.
*/
private String getLoggableAddress(HttpServletRequest request) {
// Log X-Forwarded-For, if present and valid
String header = request.getHeader("X-Forwarded-For");
if (header != null && X_FORWARDED_FOR.matcher(header).matches())
return "[" + header + ", " + request.getRemoteAddr() + "]";
// If header absent or invalid, just use source IP
return request.getRemoteAddr();
}
/**
* Attempts authentication against all AuthenticationProviders, in order,
* using the provided credentials. The first authentication failure takes
* priority, but remaining AuthenticationProviders are attempted. If any
* AuthenticationProvider succeeds, the resulting AuthenticatedUser is
* returned, and no further AuthenticationProviders are tried.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* The AuthenticatedUser given by the highest-priority
* AuthenticationProvider for which the given credentials are valid.
*
* @throws GuacamoleException
* If the given credentials are not valid for any
* AuthenticationProvider, or if an error occurs while authenticating
* the user.
*/
private AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
GuacamoleCredentialsException authFailure = null;
// Attempt authentication against each AuthenticationProvider
for (AuthenticationProvider authProvider : authProviders) {
// Attempt authentication
try {
AuthenticatedUser authenticatedUser = authProvider.authenticateUser(credentials);
if (authenticatedUser != null)
return authenticatedUser;
}
// First failure takes priority for now
catch (GuacamoleCredentialsException e) {
if (authFailure == null)
authFailure = e;
}
}
// If a specific failure occured, rethrow that
if (authFailure != null)
throw authFailure;
// Otherwise, request standard username/password
throw new GuacamoleInvalidCredentialsException(
"Permission Denied.",
CredentialsInfo.USERNAME_PASSWORD
);
}
/**
* Re-authenticates the given AuthenticatedUser against the
* AuthenticationProvider that originally created it, using the given
* Credentials.
*
* @param authenticatedUser
* The AuthenticatedUser to re-authenticate.
*
* @param credentials
* The Credentials to use to re-authenticate the user.
*
* @return
* A AuthenticatedUser which may have been updated due to re-
* authentication.
*
* @throws GuacamoleException
* If an error prevents the user from being re-authenticated.
*/
private AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleException {
// Get original AuthenticationProvider
AuthenticationProvider authProvider = authenticatedUser.getAuthenticationProvider();
// Re-authenticate the AuthenticatedUser against the original AuthenticationProvider only
authenticatedUser = authProvider.updateAuthenticatedUser(authenticatedUser, credentials);
if (authenticatedUser == null)
throw new GuacamoleSecurityException("User re-authentication failed.");
return authenticatedUser;
}
/**
* Returns the AuthenticatedUser associated with the given session and
* credentials, performing a fresh authentication and creating a new
* AuthenticatedUser if necessary.
*
* @param existingSession
* The current GuacamoleSession, or null if no session exists yet.
*
* @param credentials
* The Credentials to use to authenticate the user.
*
* @return
* The AuthenticatedUser associated with the given session and
* credentials.
*
* @throws GuacamoleException
* If an error occurs while authenticating or re-authenticating the
* user.
*/
private AuthenticatedUser getAuthenticatedUser(GuacamoleSession existingSession,
Credentials credentials) throws GuacamoleException {
try {
// Re-authenticate user if session exists
if (existingSession != null)
return updateAuthenticatedUser(existingSession.getAuthenticatedUser(), credentials);
// Otherwise, attempt authentication as a new user
AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials);
if (logger.isInfoEnabled())
logger.info("User \"{}\" successfully authenticated from {}.",
authenticatedUser.getIdentifier(),
getLoggableAddress(credentials.getRequest()));
return authenticatedUser;
}
// Log and rethrow any authentication errors
catch (GuacamoleException e) {
// Get request and username for sake of logging
HttpServletRequest request = credentials.getRequest();
String username = credentials.getUsername();
// Log authentication failures with associated usernames
if (username != null) {
if (logger.isWarnEnabled())
logger.warn("Authentication attempt from {} for user \"{}\" failed.",
getLoggableAddress(request), username);
}
// Log anonymous authentication failures
else if (logger.isDebugEnabled())
logger.debug("Anonymous authentication attempt from {} failed.",
getLoggableAddress(request));
// Rethrow exception
throw e;
}
}
/**
* Returns all UserContexts associated with the given AuthenticatedUser,
* updating existing UserContexts, if any. If no UserContexts are yet
* associated with the given AuthenticatedUser, new UserContexts are
* generated by polling each available AuthenticationProvider.
*
* @param existingSession
* The current GuacamoleSession, or null if no session exists yet.
*
* @param authenticatedUser
* The AuthenticatedUser that has successfully authenticated or re-
* authenticated.
*
* @return
* A List of all UserContexts associated with the given
* AuthenticatedUser.
*
* @throws GuacamoleException
* If an error occurs while creating or updating any UserContext.
*/
private List<UserContext> getUserContexts(GuacamoleSession existingSession,
AuthenticatedUser authenticatedUser) throws GuacamoleException {
List<UserContext> userContexts = new ArrayList<UserContext>(authProviders.size());
// If UserContexts already exist, update them and add to the list
if (existingSession != null) {
// Update all old user contexts
List<UserContext> oldUserContexts = existingSession.getUserContexts();
for (UserContext oldUserContext : oldUserContexts) {
// Update existing UserContext
AuthenticationProvider authProvider = oldUserContext.getAuthenticationProvider();
UserContext userContext = authProvider.updateUserContext(oldUserContext, authenticatedUser);
// Add to available data, if successful
if (userContext != null)
userContexts.add(userContext);
// If unsuccessful, log that this happened, as it may be a bug
else
logger.debug("AuthenticationProvider \"{}\" retroactively destroyed its UserContext.",
authProvider.getClass().getName());
}
}
// Otherwise, create new UserContexts from available AuthenticationProviders
else {
// Get UserContexts from each available AuthenticationProvider
for (AuthenticationProvider authProvider : authProviders) {
// Generate new UserContext
UserContext userContext = authProvider.getUserContext(authenticatedUser);
// Add to available data, if successful
if (userContext != null)
userContexts.add(userContext);
}
}
return userContexts;
}
/**
* Authenticates a user using the given credentials and optional
* authentication token, returning the authentication token associated with
* the user's Guacamole session, which may be newly generated. If an
* existing token is provided, the authentication procedure will attempt to
* update or reuse the provided token, but it is possible that a new token
* will be returned. Note that this function CANNOT return null.
*
* @param credentials
* The credentials to use when authenticating the user.
*
* @param token
* The authentication token to use if attempting to re-authenticate an
* existing session, or null to request a new token.
*
* @return
* The authentication token associated with the newly created or
* existing session.
*
* @throws GuacamoleException
* If the authentication or re-authentication attempt fails.
*/
public String authenticate(Credentials credentials, String token)
throws GuacamoleException {
// Pull existing session if token provided
GuacamoleSession existingSession;
if (token != null)
existingSession = tokenSessionMap.get(token);
else
existingSession = null;
// Get up-to-date AuthenticatedUser and associated UserContexts
AuthenticatedUser authenticatedUser = getAuthenticatedUser(existingSession, credentials);
List<UserContext> userContexts = getUserContexts(existingSession, authenticatedUser);
// Update existing session, if it exists
String authToken;
if (existingSession != null) {
authToken = token;
existingSession.setAuthenticatedUser(authenticatedUser);
existingSession.setUserContexts(userContexts);
}
// If no existing session, generate a new token/session pair
else {
authToken = authTokenGenerator.getToken();
tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts));
logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier());
}
return authToken;
}
/**
* Finds the Guacamole session for a given auth token, if the auth token
* represents a currently logged in user. Throws an unauthorized error
* otherwise.
*
* @param authToken The auth token to check against the map of logged in users.
* @return The session that corresponds to the provided auth token.
* @throws GuacamoleException If the auth token does not correspond to any
* logged in user.
*/
public GuacamoleSession getGuacamoleSession(String authToken)
throws GuacamoleException {
// Try to get the session from the map of logged in users.
GuacamoleSession session = tokenSessionMap.get(authToken);
// Authentication failed.
if (session == null)
throw new GuacamoleUnauthorizedException("Permission Denied.");
return session;
}
/**
* Invalidates a specific authentication token and its corresponding
* Guacamole session, effectively logging out the associated user. If the
* authentication token is not valid, this function has no effect.
*
* @param authToken
* The token being invalidated.
*
* @return
* true if the given authentication token was valid and the
* corresponding Guacamole session was destroyed, false if the given
* authentication token was not valid and no action was taken.
*/
public boolean destroyGuacamoleSession(String authToken) {
// Remove corresponding GuacamoleSession if the token is valid
GuacamoleSession session = tokenSessionMap.remove(authToken);
if (session == null)
return false;
// Invalidate the removed session
session.invalidate();
return true;
}
/**
* Returns all UserContexts associated with a given auth token, if the auth
* token represents a currently logged in user. Throws an unauthorized
* error otherwise.
*
* @param authToken
* The auth token to check against the map of logged in users.
*
* @return
* A List of all UserContexts associated with the provided auth token.
*
* @throws GuacamoleException
* If the auth token does not correspond to any logged in user.
*/
public List<UserContext> getUserContexts(String authToken)
throws GuacamoleException {
return getGuacamoleSession(authToken).getUserContexts();
}
}
@@ -0,0 +1,189 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.auth;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.properties.BasicGuacamoleProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A basic, HashMap-based implementation of the TokenSessionMap with support
* for session timeouts.
*
* @author James Muehlner
*/
public class BasicTokenSessionMap implements TokenSessionMap {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(BasicTokenSessionMap.class);
/**
* Executor service which runs the period session eviction task.
*/
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
/**
* Keeps track of the authToken to GuacamoleSession mapping.
*/
private final ConcurrentMap<String, GuacamoleSession> sessionMap =
new ConcurrentHashMap<String, GuacamoleSession>();
/**
* Create a new BasicTokenGuacamoleSessionMap configured using the given
* environment.
*
* @param environment
* The environment to use when configuring the token session map.
*/
public BasicTokenSessionMap(Environment environment) {
int sessionTimeoutValue;
// Read session timeout from guacamole.properties
try {
sessionTimeoutValue = environment.getProperty(BasicGuacamoleProperties.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);
sessionTimeoutValue = 60;
}
// Check for expired sessions every minute
logger.info("Sessions will expire after {} minutes of inactivity.", sessionTimeoutValue);
executor.scheduleAtFixedRate(new SessionEvictionTask(sessionTimeoutValue * 60000l), 1, 1, TimeUnit.MINUTES);
}
/**
* Task which iterates through all active sessions, evicting those sessions
* which are beyond the session timeout.
*/
private class SessionEvictionTask implements Runnable {
/**
* The maximum allowed age of any session, in milliseconds.
*/
private final long sessionTimeout;
/**
* Creates a new task which automatically evicts sessions which are
* older than the specified timeout.
*
* @param sessionTimeout The maximum age of any session, in
* milliseconds.
*/
public SessionEvictionTask(long sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
@Override
public void run() {
// Get start time of session check time
long sessionCheckStart = System.currentTimeMillis();
logger.debug("Checking for expired sessions...");
// For each session, remove sesions which have expired
Iterator<Map.Entry<String, GuacamoleSession>> entries = sessionMap.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<String, GuacamoleSession> entry = entries.next();
GuacamoleSession session = entry.getValue();
// Do not expire sessions which are active
if (session.hasTunnels())
continue;
// Get elapsed time since last access
long age = sessionCheckStart - session.getLastAccessedTime();
// If session is too old, evict it and check the next one
if (age >= sessionTimeout) {
logger.debug("Session \"{}\" has timed out.", entry.getKey());
entries.remove();
session.invalidate();
}
}
// Log completion and duration
logger.debug("Session check completed in {} ms.",
System.currentTimeMillis() - sessionCheckStart);
}
}
@Override
public GuacamoleSession get(String authToken) {
// There are no null auth tokens
if (authToken == null)
return null;
// Update the last access time and return the GuacamoleSession
GuacamoleSession session = sessionMap.get(authToken);
if (session != null)
session.access();
return session;
}
@Override
public void put(String authToken, GuacamoleSession session) {
sessionMap.put(authToken, session);
}
@Override
public GuacamoleSession remove(String authToken) {
// There are no null auth tokens
if (authToken == null)
return null;
// Attempt to retrieve only if non-null
return sessionMap.remove(authToken);
}
@Override
public void shutdown() {
executor.shutdownNow();
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.auth;
import java.security.SecureRandom;
import org.apache.commons.codec.binary.Hex;
/**
* An implementation of the AuthTokenGenerator based around SecureRandom.
*
* @author James Muehlner
*/
public class SecureRandomAuthTokenGenerator implements AuthTokenGenerator {
/**
* Instance of SecureRandom for generating the auth token.
*/
private final SecureRandom secureRandom = new SecureRandom();
@Override
public String getToken() {
byte[] bytes = new byte[32];
secureRandom.nextBytes(bytes);
return Hex.encodeHexString(bytes);
}
}
@@ -0,0 +1,230 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.auth;
import com.google.inject.Inject;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.xml.bind.DatatypeConverter;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.rest.APIRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A service for managing auth tokens via the Guacamole REST API.
*
* @author James Muehlner
* @author Michael Jumper
*/
@Path("/tokens")
@Produces(MediaType.APPLICATION_JSON)
public class TokenRESTService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(TokenRESTService.class);
/**
* Service for authenticating users and managing their Guacamole sessions.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Returns the credentials associated with the given request, using the
* provided username and password.
*
* @param request
* The request to use to derive the credentials.
*
* @param username
* The username to associate with the credentials, or null if the
* username should be derived from the request.
*
* @param password
* The password to associate with the credentials, or null if the
* password should be derived from the request.
*
* @return
* A new Credentials object whose contents have been derived from the
* given request, along with the provided username and password.
*/
private Credentials getCredentials(HttpServletRequest request,
String username, String password) {
// If no username/password given, try Authorization header
if (username == null && password == null) {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Basic ")) {
try {
// Decode base64 authorization
String basicBase64 = authorization.substring(6);
String basicCredentials = new String(DatatypeConverter.parseBase64Binary(basicBase64), "UTF-8");
// Pull username/password from auth data
int colon = basicCredentials.indexOf(':');
if (colon != -1) {
username = basicCredentials.substring(0, colon);
password = basicCredentials.substring(colon + 1);
}
else
logger.debug("Invalid HTTP Basic \"Authorization\" header received.");
}
// UTF-8 support is required by the Java specification
catch (UnsupportedEncodingException e) {
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
}
}
} // end Authorization header fallback
// Build credentials
Credentials credentials = new Credentials();
credentials.setUsername(username);
credentials.setPassword(password);
credentials.setRequest(request);
credentials.setSession(request.getSession(true));
return credentials;
}
/**
* Authenticates a user, generates an auth token, associates that auth token
* with the user's UserContext for use by further requests. If an existing
* token is provided, the authentication procedure will attempt to update
* or reuse the provided token.
*
* @param username
* The username of the user who is to be authenticated.
*
* @param password
* The password of the user who is to be authenticated.
*
* @param token
* An optional existing auth token for the user who is to be
* authenticated.
*
* @param consumedRequest
* The HttpServletRequest associated with the login attempt. The
* parameters of this request may not be accessible, as the request may
* have been fully consumed by JAX-RS.
*
* @param parameters
* A MultivaluedMap containing all parameters from the given HTTP
* request. All request parameters must be made available through this
* map, even if those parameters are no longer accessible within the
* now-fully-consumed HTTP request.
*
* @return
* An authentication response object containing the possible-new auth
* token, as well as other related data.
*
* @throws GuacamoleException
* If an error prevents successful authentication.
*/
@POST
public APIAuthenticationResult createToken(@FormParam("username") String username,
@FormParam("password") String password,
@FormParam("token") String token,
@Context HttpServletRequest consumedRequest,
MultivaluedMap<String, String> parameters)
throws GuacamoleException {
// Reconstitute the HTTP request with the map of parameters
HttpServletRequest request = new APIRequest(consumedRequest, parameters);
// Build credentials from request
Credentials credentials = getCredentials(request, username, password);
// Create/update session producing possibly-new token
token = authenticationService.authenticate(credentials, token);
// Pull corresponding session
GuacamoleSession session = authenticationService.getGuacamoleSession(token);
if (session == null)
throw new GuacamoleResourceNotFoundException("No such token.");
// Build list of all available auth providers
List<UserContext> userContexts = session.getUserContexts();
List<String> authProviderIdentifiers = new ArrayList<String>(userContexts.size());
for (UserContext userContext : userContexts)
authProviderIdentifiers.add(userContext.getAuthenticationProvider().getIdentifier());
// Return possibly-new auth token
AuthenticatedUser authenticatedUser = session.getAuthenticatedUser();
return new APIAuthenticationResult(
token,
authenticatedUser.getIdentifier(),
authenticatedUser.getAuthenticationProvider().getIdentifier(),
authProviderIdentifiers
);
}
/**
* Invalidates a specific auth token, effectively logging out the associated
* user.
*
* @param authToken
* The token being invalidated.
*
* @throws GuacamoleException
* If the specified token does not exist.
*/
@DELETE
@Path("/{token}")
public void invalidateToken(@PathParam("token") String authToken)
throws GuacamoleException {
// Invalidate session, if it exists
if (!authenticationService.destroyGuacamoleSession(authToken))
throw new GuacamoleResourceNotFoundException("No such token.");
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.auth;
import org.apache.guacamole.net.basic.GuacamoleSession;
/**
* Represents a mapping of auth token to Guacamole session for the REST
* authentication system.
*
* @author James Muehlner
*/
public interface TokenSessionMap {
/**
* Registers that a user has just logged in with the specified authToken and
* GuacamoleSession.
*
* @param authToken The authentication token for the logged in user.
* @param session The GuacamoleSession for the logged in user.
*/
public void put(String authToken, GuacamoleSession session);
/**
* Get the GuacamoleSession for a logged in user. If the auth token does not
* represent a user who is currently logged in, returns null.
*
* @param authToken The authentication token for the logged in user.
* @return The GuacamoleSession for the given auth token, if the auth token
* represents a currently logged in user, null otherwise.
*/
public GuacamoleSession get(String authToken);
/**
* Removes the GuacamoleSession associated with the given auth token.
*
* @param authToken The token to remove.
* @return The GuacamoleSession for the given auth token, if the auth token
* represents a currently logged in user, null otherwise.
*/
public GuacamoleSession remove(String authToken);
/**
* Shuts down this session map, disallowing future sessions and reclaiming
* any resources.
*/
public void shutdown();
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the authentication aspect of the Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest.auth;
@@ -0,0 +1,233 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.connection;
import java.util.Map;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
/**
* A simple connection to expose through the REST endpoints.
*
* @author James Muehlner
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class APIConnection {
/**
* The name of this connection.
*/
private String name;
/**
* The identifier of this connection.
*/
private String identifier;
/**
* The identifier of the parent connection group for this connection.
*/
private String parentIdentifier;
/**
* The protocol of this connection.
*/
private String protocol;
/**
* Map of all associated parameter values, indexed by parameter name.
*/
private Map<String, String> parameters;
/**
* Map of all associated attributes by attribute identifier.
*/
private Map<String, String> attributes;
/**
* The count of currently active connections using this connection.
*/
private int activeConnections;
/**
* Create an empty APIConnection.
*/
public APIConnection() {}
/**
* Create an APIConnection from a Connection record. Parameters for the
* connection will not be included.
*
* @param connection The connection to create this APIConnection from.
* @throws GuacamoleException If a problem is encountered while
* instantiating this new APIConnection.
*/
public APIConnection(Connection connection)
throws GuacamoleException {
// Set connection information
this.name = connection.getName();
this.identifier = connection.getIdentifier();
this.parentIdentifier = connection.getParentIdentifier();
this.activeConnections = connection.getActiveConnections();
// Set protocol from configuration
GuacamoleConfiguration configuration = connection.getConfiguration();
this.protocol = configuration.getProtocol();
// Associate any attributes
this.attributes = connection.getAttributes();
}
/**
* Returns the name of this connection.
* @return The name of this connection.
*/
public String getName() {
return name;
}
/**
* Set the name of this connection.
* @param name The name of this connection.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns the unique identifier for this connection.
* @return The unique identifier for this connection.
*/
public String getIdentifier() {
return identifier;
}
/**
* Sets the unique identifier for this connection.
* @param identifier The unique identifier for this connection.
*/
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
/**
* Returns the unique identifier for this connection.
* @return The unique identifier for this connection.
*/
public String getParentIdentifier() {
return parentIdentifier;
}
/**
* Sets the parent connection group identifier for this connection.
* @param parentIdentifier The parent connection group identifier
* for this connection.
*/
public void setParentIdentifier(String parentIdentifier) {
this.parentIdentifier = parentIdentifier;
}
/**
* Returns the parameter map for this connection.
* @return The parameter map for this connection.
*/
public Map<String, String> getParameters() {
return parameters;
}
/**
* Sets the parameter map for this connection.
* @param parameters The parameter map for this connection.
*/
public void setParameters(Map<String, String> parameters) {
this.parameters = parameters;
}
/**
* Returns the protocol for this connection.
* @return The protocol for this connection.
*/
public String getProtocol() {
return protocol;
}
/**
* Sets the protocol for this connection.
* @param protocol protocol for this connection.
*/
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
* Returns the number of currently active connections using this
* connection.
*
* @return
* The number of currently active usages of this connection.
*/
public int getActiveConnections() {
return activeConnections;
}
/**
* Set the number of currently active connections using this connection.
*
* @param activeConnections
* The number of currently active usages of this connection.
*/
public void setActiveUsers(int activeConnections) {
this.activeConnections = activeConnections;
}
/**
* Returns a map of all attributes associated with this connection. Each
* entry key is the attribute identifier, while each value is the attribute
* value itself.
*
* @return
* The attribute map for this connection.
*/
public Map<String, String> getAttributes() {
return attributes;
}
/**
* Sets the map of all attributes associated with this connection. Each
* entry key is the attribute identifier, while each value is the attribute
* value itself.
*
* @param attributes
* The attribute map for this connection.
*/
public void setAttributes(Map<String, String> attributes) {
this.attributes = attributes;
}
}
@@ -0,0 +1,138 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.connection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.protocol.GuacamoleClientInformation;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
/**
* A wrapper to make an APIConnection look like a Connection. Useful where a
* org.apache.guacamole.net.auth.Connection is required.
*
* @author James Muehlner
*/
public class APIConnectionWrapper implements Connection {
/**
* The wrapped APIConnection.
*/
private final APIConnection apiConnection;
/**
* Creates a new APIConnectionWrapper which wraps the given APIConnection
* as a Connection.
*
* @param apiConnection
* The APIConnection to wrap.
*/
public APIConnectionWrapper(APIConnection apiConnection) {
this.apiConnection = apiConnection;
}
@Override
public String getName() {
return apiConnection.getName();
}
@Override
public void setName(String name) {
apiConnection.setName(name);
}
@Override
public String getIdentifier() {
return apiConnection.getIdentifier();
}
@Override
public void setIdentifier(String identifier) {
apiConnection.setIdentifier(identifier);
}
@Override
public String getParentIdentifier() {
return apiConnection.getParentIdentifier();
}
@Override
public void setParentIdentifier(String parentIdentifier) {
apiConnection.setParentIdentifier(parentIdentifier);
}
@Override
public int getActiveConnections() {
return apiConnection.getActiveConnections();
}
@Override
public GuacamoleConfiguration getConfiguration() {
// Create the GuacamoleConfiguration with current protocol
GuacamoleConfiguration configuration = new GuacamoleConfiguration();
configuration.setProtocol(apiConnection.getProtocol());
// Add parameters, if available
Map<String, String> parameters = apiConnection.getParameters();
if (parameters != null)
configuration.setParameters(parameters);
return configuration;
}
@Override
public void setConfiguration(GuacamoleConfiguration config) {
// Set protocol and parameters
apiConnection.setProtocol(config.getProtocol());
apiConnection.setParameters(config.getParameters());
}
@Override
public Map<String, String> getAttributes() {
return apiConnection.getAttributes();
}
@Override
public void setAttributes(Map<String, String> attributes) {
apiConnection.setAttributes(attributes);
}
@Override
public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException {
throw new UnsupportedOperationException("Operation not supported.");
}
@Override
public List<? extends ConnectionRecord> getHistory() throws GuacamoleException {
return Collections.<ConnectionRecord>emptyList();
}
}
@@ -0,0 +1,349 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.connection;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.rest.ObjectRetrievalService;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.apache.guacamole.net.basic.rest.history.APIConnectionRecord;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A REST Service for handling connection CRUD operations.
*
* @author James Muehlner
*/
@Path("/data/{dataSource}/connections")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ConnectionRESTService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ConnectionRESTService.class);
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/**
* Retrieves an individual connection.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection to be retrieved.
*
* @param connectionID
* The identifier of the connection to retrieve.
*
* @return
* The connection having the given identifier.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection.
*/
@GET
@Path("/{connectionID}")
public APIConnection getConnection(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionID") String connectionID)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
// Retrieve the requested connection
return new APIConnection(retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID));
}
/**
* Retrieves the parameters associated with a single connection.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection whose parameters are to be
* retrieved.
*
* @param connectionID
* The identifier of the connection.
*
* @return
* A map of parameter name/value pairs.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection parameters.
*/
@GET
@Path("/{connectionID}/parameters")
public Map<String, String> getConnectionParameters(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionID") String connectionID)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
User self = userContext.self();
// Retrieve permission sets
SystemPermissionSet systemPermissions = self.getSystemPermissions();
ObjectPermissionSet connectionPermissions = self.getConnectionPermissions();
// Deny access if adminstrative or update permission is missing
if (!systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER)
&& !connectionPermissions.hasPermission(ObjectPermission.Type.UPDATE, connectionID))
throw new GuacamoleSecurityException("Permission to read connection parameters denied.");
// Retrieve the requested connection
Connection connection = retrievalService.retrieveConnection(userContext, connectionID);
// Retrieve connection configuration
GuacamoleConfiguration config = connection.getConfiguration();
// Return parameter map
return config.getParameters();
}
/**
* Retrieves the usage history of a single connection.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection whose history is to be
* retrieved.
*
* @param connectionID
* The identifier of the connection.
*
* @return
* A list of connection records, describing the start and end times of
* various usages of this connection.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection history.
*/
@GET
@Path("/{connectionID}/history")
public List<APIConnectionRecord> getConnectionHistory(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionID") String connectionID)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
// Retrieve the requested connection
Connection connection = retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID);
// Retrieve the requested connection's history
List<APIConnectionRecord> apiRecords = new ArrayList<APIConnectionRecord>();
for (ConnectionRecord record : connection.getHistory())
apiRecords.add(new APIConnectionRecord(record));
// Return the converted history
return apiRecords;
}
/**
* Deletes an individual connection.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection to be deleted.
*
* @param connectionID
* The identifier of the connection to delete.
*
* @throws GuacamoleException
* If an error occurs while deleting the connection.
*/
@DELETE
@Path("/{connectionID}")
public void deleteConnection(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionID") String connectionID)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the connection directory
Directory<Connection> connectionDirectory = userContext.getConnectionDirectory();
// Delete the specified connection
connectionDirectory.remove(connectionID);
}
/**
* Creates a new connection and returns the new connection, with identifier
* field populated.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext in which the connection is to be created.
*
* @param connection
* The connection to create.
*
* @return
* The new connection.
*
* @throws GuacamoleException
* If an error occurs while creating the connection.
*/
@POST
public APIConnection createConnection(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
APIConnection connection) throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Validate that connection data was provided
if (connection == null)
throw new GuacamoleClientException("Connection JSON must be submitted when creating connections.");
// Add the new connection
Directory<Connection> connectionDirectory = userContext.getConnectionDirectory();
connectionDirectory.add(new APIConnectionWrapper(connection));
// Return the new connection
return connection;
}
/**
* Updates an existing connection. If the parent identifier of the
* connection is changed, the connection will also be moved to the new
* parent group.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection to be updated.
*
* @param connectionID
* The identifier of the connection to update.
*
* @param connection
* The connection data to update the specified connection with.
*
* @throws GuacamoleException
* If an error occurs while updating the connection.
*/
@PUT
@Path("/{connectionID}")
public void updateConnection(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionID") String connectionID,
APIConnection connection) throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Validate that connection data was provided
if (connection == null)
throw new GuacamoleClientException("Connection JSON must be submitted when updating connections.");
// Get the connection directory
Directory<Connection> connectionDirectory = userContext.getConnectionDirectory();
// Retrieve connection to update
Connection existingConnection = retrievalService.retrieveConnection(userContext, connectionID);
// Build updated configuration
GuacamoleConfiguration config = new GuacamoleConfiguration();
config.setProtocol(connection.getProtocol());
config.setParameters(connection.getParameters());
// Update the connection
existingConnection.setConfiguration(config);
existingConnection.setParentIdentifier(connection.getParentIdentifier());
existingConnection.setName(connection.getName());
existingConnection.setAttributes(connection.getAttributes());
connectionDirectory.update(existingConnection);
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the connection manipulation aspect of the Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest.connection;
@@ -0,0 +1,272 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.connectiongroup;
import java.util.Collection;
import java.util.Map;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.ConnectionGroup.Type;
import org.apache.guacamole.net.basic.rest.connection.APIConnection;
/**
* A simple connection group to expose through the REST endpoints.
*
* @author James Muehlner
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class APIConnectionGroup {
/**
* The identifier of the root connection group.
*/
public static final String ROOT_IDENTIFIER = "ROOT";
/**
* The name of this connection group.
*/
private String name;
/**
* The identifier of this connection group.
*/
private String identifier;
/**
* The identifier of the parent connection group for this connection group.
*/
private String parentIdentifier;
/**
* The type of this connection group.
*/
private Type type;
/**
* The count of currently active connections using this connection group.
*/
private int activeConnections;
/**
* All child connection groups. If children are not being queried, this may
* be omitted.
*/
private Collection<APIConnectionGroup> childConnectionGroups;
/**
* All child connections. If children are not being queried, this may be
* omitted.
*/
private Collection<APIConnection> childConnections;
/**
* Map of all associated attributes by attribute identifier.
*/
private Map<String, String> attributes;
/**
* Create an empty APIConnectionGroup.
*/
public APIConnectionGroup() {}
/**
* Create a new APIConnectionGroup from the given ConnectionGroup record.
*
* @param connectionGroup The ConnectionGroup record to initialize this
* APIConnectionGroup from.
*/
public APIConnectionGroup(ConnectionGroup connectionGroup) {
// Set connection group information
this.identifier = connectionGroup.getIdentifier();
this.parentIdentifier = connectionGroup.getParentIdentifier();
this.name = connectionGroup.getName();
this.type = connectionGroup.getType();
this.activeConnections = connectionGroup.getActiveConnections();
// Associate any attributes
this.attributes = connectionGroup.getAttributes();
}
/**
* Returns the name of this connection group.
* @return The name of this connection group.
*/
public String getName() {
return name;
}
/**
* Set the name of this connection group.
* @param name The name of this connection group.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns the identifier of this connection group.
* @return The identifier of this connection group.
*/
public String getIdentifier() {
return identifier;
}
/**
* Set the identifier of this connection group.
* @param identifier The identifier of this connection group.
*/
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
/**
* Returns the unique identifier for this connection group.
* @return The unique identifier for this connection group.
*/
public String getParentIdentifier() {
return parentIdentifier;
}
/**
* Sets the parent connection group identifier for this connection group.
* @param parentIdentifier The parent connection group identifier
* for this connection group.
*/
public void setParentIdentifier(String parentIdentifier) {
this.parentIdentifier = parentIdentifier;
}
/**
* Returns the type of this connection group.
* @return The type of this connection group.
*/
public Type getType() {
return type;
}
/**
* Set the type of this connection group.
* @param type The Type of this connection group.
*/
public void setType(Type type) {
this.type = type;
}
/**
* Returns a collection of all child connection groups, or null if children
* have not been queried.
*
* @return
* A collection of all child connection groups, or null if children
* have not been queried.
*/
public Collection<APIConnectionGroup> getChildConnectionGroups() {
return childConnectionGroups;
}
/**
* Sets the collection of all child connection groups to the given
* collection, which may be null if children have not been queried.
*
* @param childConnectionGroups
* The collection containing all child connection groups of this
* connection group, or null if children have not been queried.
*/
public void setChildConnectionGroups(Collection<APIConnectionGroup> childConnectionGroups) {
this.childConnectionGroups = childConnectionGroups;
}
/**
* Returns a collection of all child connections, or null if children have
* not been queried.
*
* @return
* A collection of all child connections, or null if children have not
* been queried.
*/
public Collection<APIConnection> getChildConnections() {
return childConnections;
}
/**
* Sets the collection of all child connections to the given collection,
* which may be null if children have not been queried.
*
* @param childConnections
* The collection containing all child connections of this connection
* group, or null if children have not been queried.
*/
public void setChildConnections(Collection<APIConnection> childConnections) {
this.childConnections = childConnections;
}
/**
* Returns the number of currently active connections using this
* connection group.
*
* @return
* The number of currently active usages of this connection group.
*/
public int getActiveConnections() {
return activeConnections;
}
/**
* Set the number of currently active connections using this connection
* group.
*
* @param activeConnections
* The number of currently active usages of this connection group.
*/
public void setActiveUsers(int activeConnections) {
this.activeConnections = activeConnections;
}
/**
* Returns a map of all attributes associated with this connection group.
* Each entry key is the attribute identifier, while each value is the
* attribute value itself.
*
* @return
* The attribute map for this connection group.
*/
public Map<String, String> getAttributes() {
return attributes;
}
/**
* Sets the map of all attributes associated with this connection group.
* Each entry key is the attribute identifier, while each value is the
* attribute value itself.
*
* @param attributes
* The attribute map for this connection group.
*/
public void setAttributes(Map<String, String> attributes) {
this.attributes = attributes;
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.connectiongroup;
import java.util.Map;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.protocol.GuacamoleClientInformation;
/**
* A wrapper to make an APIConnection look like a ConnectionGroup.
* Useful where a org.apache.guacamole.net.auth.ConnectionGroup is required.
*
* @author James Muehlner
*/
public class APIConnectionGroupWrapper implements ConnectionGroup {
/**
* The wrapped APIConnectionGroup.
*/
private final APIConnectionGroup apiConnectionGroup;
/**
* Create a new APIConnectionGroupWrapper to wrap the given
* APIConnectionGroup as a ConnectionGroup.
* @param apiConnectionGroup the APIConnectionGroup to wrap.
*/
public APIConnectionGroupWrapper(APIConnectionGroup apiConnectionGroup) {
this.apiConnectionGroup = apiConnectionGroup;
}
@Override
public String getName() {
return apiConnectionGroup.getName();
}
@Override
public void setName(String name) {
apiConnectionGroup.setName(name);
}
@Override
public String getIdentifier() {
return apiConnectionGroup.getIdentifier();
}
@Override
public void setIdentifier(String identifier) {
apiConnectionGroup.setIdentifier(identifier);
}
@Override
public String getParentIdentifier() {
return apiConnectionGroup.getParentIdentifier();
}
@Override
public void setParentIdentifier(String parentIdentifier) {
apiConnectionGroup.setParentIdentifier(parentIdentifier);
}
@Override
public void setType(Type type) {
apiConnectionGroup.setType(type);
}
@Override
public Type getType() {
return apiConnectionGroup.getType();
}
@Override
public int getActiveConnections() {
return apiConnectionGroup.getActiveConnections();
}
@Override
public Set<String> getConnectionIdentifiers() {
throw new UnsupportedOperationException("Operation not supported.");
}
@Override
public Set<String> getConnectionGroupIdentifiers() {
throw new UnsupportedOperationException("Operation not supported.");
}
@Override
public Map<String, String> getAttributes() {
return apiConnectionGroup.getAttributes();
}
@Override
public void setAttributes(Map<String, String> attributes) {
apiConnectionGroup.setAttributes(attributes);
}
@Override
public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException {
throw new UnsupportedOperationException("Operation not supported.");
}
}
@@ -0,0 +1,287 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.connectiongroup;
import com.google.inject.Inject;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.rest.ObjectRetrievalService;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A REST Service for handling connection group CRUD operations.
*
* @author James Muehlner
*/
@Path("/data/{dataSource}/connectionGroups")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ConnectionGroupRESTService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ConnectionGroupRESTService.class);
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/**
* Gets an individual connection group.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection group to be retrieved.
*
* @param connectionGroupID
* The ID of the connection group to retrieve.
*
* @return
* The connection group, without any descendants.
*
* @throws GuacamoleException
* If a problem is encountered while retrieving the connection group.
*/
@GET
@Path("/{connectionGroupID}")
public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionGroupID") String connectionGroupID)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
// Retrieve the requested connection group
return new APIConnectionGroup(retrievalService.retrieveConnectionGroup(session, authProviderIdentifier, connectionGroupID));
}
/**
* Gets an individual connection group and all children.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection group to be retrieved.
*
* @param connectionGroupID
* The ID of the connection group to retrieve.
*
* @param permissions
* If specified and non-empty, limit the returned list to only those
* connections for which the current user has any of the given
* permissions. Otherwise, all visible connections are returned.
* Connection groups are unaffected by this parameter.
*
* @return
* The requested connection group, including all descendants.
*
* @throws GuacamoleException
* If a problem is encountered while retrieving the connection group or
* its descendants.
*/
@GET
@Path("/{connectionGroupID}/tree")
public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionGroupID") String connectionGroupID,
@QueryParam("permission") List<ObjectPermission.Type> permissions)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Retrieve the requested tree, filtering by the given permissions
ConnectionGroup treeRoot = retrievalService.retrieveConnectionGroup(userContext, connectionGroupID);
ConnectionGroupTree tree = new ConnectionGroupTree(userContext, treeRoot, permissions);
// Return tree as a connection group
return tree.getRootAPIConnectionGroup();
}
/**
* Deletes an individual connection group.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection group to be deleted.
*
* @param connectionGroupID
* The identifier of the connection group to delete.
*
* @throws GuacamoleException
* If an error occurs while deleting the connection group.
*/
@DELETE
@Path("/{connectionGroupID}")
public void deleteConnectionGroup(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionGroupID") String connectionGroupID)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the connection group directory
Directory<ConnectionGroup> connectionGroupDirectory = userContext.getConnectionGroupDirectory();
// Delete the connection group
connectionGroupDirectory.remove(connectionGroupID);
}
/**
* Creates a new connection group and returns the new connection group,
* with identifier field populated.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext in which the connection group is to be created.
*
* @param connectionGroup
* The connection group to create.
*
* @return
* The new connection group.
*
* @throws GuacamoleException
* If an error occurs while creating the connection group.
*/
@POST
public APIConnectionGroup createConnectionGroup(
@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
APIConnectionGroup connectionGroup) throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Validate that connection group data was provided
if (connectionGroup == null)
throw new GuacamoleClientException("Connection group JSON must be submitted when creating connections groups.");
// Add the new connection group
Directory<ConnectionGroup> connectionGroupDirectory = userContext.getConnectionGroupDirectory();
connectionGroupDirectory.add(new APIConnectionGroupWrapper(connectionGroup));
// Return the new connection group
return connectionGroup;
}
/**
* Updates a connection group. If the parent identifier of the
* connection group is changed, the connection group will also be moved to
* the new parent group.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection group to be updated.
*
* @param connectionGroupID
* The identifier of the existing connection group to update.
*
* @param connectionGroup
* The data to update the existing connection group with.
*
* @throws GuacamoleException
* If an error occurs while updating the connection group.
*/
@PUT
@Path("/{connectionGroupID}")
public void updateConnectionGroup(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("connectionGroupID") String connectionGroupID,
APIConnectionGroup connectionGroup)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Validate that connection group data was provided
if (connectionGroup == null)
throw new GuacamoleClientException("Connection group JSON must be submitted when updating connection groups.");
// Get the connection group directory
Directory<ConnectionGroup> connectionGroupDirectory = userContext.getConnectionGroupDirectory();
// Retrieve connection group to update
ConnectionGroup existingConnectionGroup = retrievalService.retrieveConnectionGroup(userContext, connectionGroupID);
// Update the connection group
existingConnectionGroup.setName(connectionGroup.getName());
existingConnectionGroup.setParentIdentifier(connectionGroup.getParentIdentifier());
existingConnectionGroup.setType(connectionGroup.getType());
existingConnectionGroup.setAttributes(connectionGroup.getAttributes());
connectionGroupDirectory.update(existingConnectionGroup);
}
}
@@ -0,0 +1,259 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.connectiongroup;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.basic.rest.connection.APIConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides access to the entire tree of connection groups and their
* connections.
*
* @author Michael Jumper
*/
public class ConnectionGroupTree {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ConnectionGroupTree.class);
/**
* The context of the user obtaining this tree.
*/
private final UserContext userContext;
/**
* The root connection group as an APIConnectionGroup.
*/
private final APIConnectionGroup rootAPIGroup;
/**
* All connection groups that have been retrieved, stored by their
* identifiers.
*/
private final Map<String, APIConnectionGroup> retrievedGroups =
new HashMap<String, APIConnectionGroup>();
/**
* Adds each of the provided connections to the current tree as children
* of their respective parents. The parent connection groups must already
* be added.
*
* @param connections
* The connections to add to the tree.
*
* @throws GuacamoleException
* If an error occurs while adding the connection to the tree.
*/
private void addConnections(Collection<Connection> connections)
throws GuacamoleException {
// Add each connection to the tree
for (Connection connection : connections) {
// Retrieve the connection's parent group
APIConnectionGroup parent = retrievedGroups.get(connection.getParentIdentifier());
if (parent != null) {
Collection<APIConnection> children = parent.getChildConnections();
// Create child collection if it does not yet exist
if (children == null) {
children = new ArrayList<APIConnection>();
parent.setChildConnections(children);
}
// Add child
children.add(new APIConnection(connection));
}
// Warn of internal consistency issues
else
logger.debug("Connection \"{}\" cannot be added to the tree: parent \"{}\" does not actually exist.",
connection.getIdentifier(),
connection.getParentIdentifier());
} // end for each connection
}
/**
* Adds each of the provided connection groups to the current tree as
* children of their respective parents. The parent connection groups must
* already be added.
*
* @param connectionGroups
* The connection groups to add to the tree.
*/
private void addConnectionGroups(Collection<ConnectionGroup> connectionGroups) {
// Add each connection group to the tree
for (ConnectionGroup connectionGroup : connectionGroups) {
// Retrieve the connection group's parent group
APIConnectionGroup parent = retrievedGroups.get(connectionGroup.getParentIdentifier());
if (parent != null) {
Collection<APIConnectionGroup> children = parent.getChildConnectionGroups();
// Create child collection if it does not yet exist
if (children == null) {
children = new ArrayList<APIConnectionGroup>();
parent.setChildConnectionGroups(children);
}
// Add child
APIConnectionGroup apiConnectionGroup = new APIConnectionGroup(connectionGroup);
retrievedGroups.put(connectionGroup.getIdentifier(), apiConnectionGroup);
children.add(apiConnectionGroup);
}
// Warn of internal consistency issues
else
logger.debug("Connection group \"{}\" cannot be added to the tree: parent \"{}\" does not actually exist.",
connectionGroup.getIdentifier(),
connectionGroup.getParentIdentifier());
} // end for each connection group
}
/**
* Adds all descendants of the given parent groups to their corresponding
* parents already stored under root.
*
* @param parents
* The parents whose descendants should be added to the tree.
*
* @param permissions
* If specified and non-empty, limit added connections to only
* connections for which the current user has any of the given
* permissions. Otherwise, all visible connections are added.
* Connection groups are unaffected by this parameter.
*
* @throws GuacamoleException
* If an error occurs while retrieving the descendants.
*/
private void addDescendants(Collection<ConnectionGroup> parents,
List<ObjectPermission.Type> permissions)
throws GuacamoleException {
// If no parents, nothing to do
if (parents.isEmpty())
return;
Collection<String> childConnectionIdentifiers = new ArrayList<String>();
Collection<String> childConnectionGroupIdentifiers = new ArrayList<String>();
// Build lists of identifiers for retrieval
for (ConnectionGroup parent : parents) {
childConnectionIdentifiers.addAll(parent.getConnectionIdentifiers());
childConnectionGroupIdentifiers.addAll(parent.getConnectionGroupIdentifiers());
}
// Filter identifiers based on permissions, if requested
if (permissions != null && !permissions.isEmpty()) {
ObjectPermissionSet permissionSet = userContext.self().getConnectionPermissions();
childConnectionIdentifiers = permissionSet.getAccessibleObjects(permissions, childConnectionIdentifiers);
}
// Retrieve child connections
if (!childConnectionIdentifiers.isEmpty()) {
Collection<Connection> childConnections = userContext.getConnectionDirectory().getAll(childConnectionIdentifiers);
addConnections(childConnections);
}
// Retrieve child connection groups
if (!childConnectionGroupIdentifiers.isEmpty()) {
Collection<ConnectionGroup> childConnectionGroups = userContext.getConnectionGroupDirectory().getAll(childConnectionGroupIdentifiers);
addConnectionGroups(childConnectionGroups);
addDescendants(childConnectionGroups, permissions);
}
}
/**
* Creates a new connection group tree using the given connection group as
* the tree root.
*
* @param userContext
* The context of the user obtaining the connection group tree.
*
* @param root
* The connection group to use as the root of this connection group
* tree.
*
* @param permissions
* If specified and non-empty, limit the contents of the tree to only
* those connections for which the current user has any of the given
* permissions. Otherwise, all visible connections are returned.
* Connection groups are unaffected by this parameter.
*
* @throws GuacamoleException
* If an error occurs while retrieving the tree of connection groups
* and their descendants.
*/
public ConnectionGroupTree(UserContext userContext, ConnectionGroup root,
List<ObjectPermission.Type> permissions) throws GuacamoleException {
this.userContext = userContext;
// Store root of tree
this.rootAPIGroup = new APIConnectionGroup(root);
retrievedGroups.put(root.getIdentifier(), this.rootAPIGroup);
// Add all descendants
addDescendants(Collections.singleton(root), permissions);
}
/**
* Returns the entire connection group tree as an APIConnectionGroup. The
* returned APIConnectionGroup is the root group and will contain all
* descendant connection groups and connections, arranged hierarchically.
*
* @return
* The root connection group, containing the entire connection group
* tree and all connections.
*/
public APIConnectionGroup getRootAPIConnectionGroup() {
return rootAPIGroup;
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the connection group manipulation aspect
* of the Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest.connectiongroup;
@@ -0,0 +1,163 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.history;
import java.util.Date;
import org.apache.guacamole.net.auth.ConnectionRecord;
/**
* A connection record which may be exposed through the REST endpoints.
*
* @author Michael Jumper
*/
public class APIConnectionRecord {
/**
* The identifier of the connection associated with this record.
*/
private final String connectionIdentifier;
/**
* The identifier of the connection associated with this record.
*/
private final String connectionName;
/**
* The date and time the connection began.
*/
private final Date startDate;
/**
* The date and time the connection ended, or null if the connection is
* still running or if the end time is unknown.
*/
private final Date endDate;
/**
* The host from which the connection originated, if known.
*/
private final String remoteHost;
/**
* The name of the user who used or is using the connection.
*/
private final String username;
/**
* Whether the connection is currently active.
*/
private final boolean active;
/**
* Creates a new APIConnectionRecord, copying the data from the given
* record.
*
* @param record
* The record to copy data from.
*/
public APIConnectionRecord(ConnectionRecord record) {
this.connectionIdentifier = record.getConnectionIdentifier();
this.connectionName = record.getConnectionName();
this.startDate = record.getStartDate();
this.endDate = record.getEndDate();
this.remoteHost = record.getRemoteHost();
this.username = record.getUsername();
this.active = record.isActive();
}
/**
* Returns the identifier of the connection associated with this
* record.
*
* @return
* The identifier of the connection associated with this record.
*/
public String getConnectionIdentifier() {
return connectionIdentifier;
}
/**
* Returns the name of the connection associated with this record.
*
* @return
* The name of the connection associated with this record.
*/
public String getConnectionName() {
return connectionName;
}
/**
* Returns the date and time the connection began.
*
* @return
* The date and time the connection began.
*/
public Date getStartDate() {
return startDate;
}
/**
* Returns the date and time the connection ended, if applicable.
*
* @return
* The date and time the connection ended, or null if the connection is
* still running or if the end time is unknown.
*/
public Date getEndDate() {
return endDate;
}
/**
* Returns the remote host from which this connection originated.
*
* @return
* The remote host from which this connection originated.
*/
public String getRemoteHost() {
return remoteHost;
}
/**
* Returns the name of the user who used or is using the connection at the
* times given by this connection record.
*
* @return
* The name of the user who used or is using the associated connection.
*/
public String getUsername() {
return username;
}
/**
* Returns whether the connection associated with this record is still
* active.
*
* @return
* true if the connection associated with this record is still active,
* false otherwise.
*/
public boolean isActive() {
return active;
}
}
@@ -0,0 +1,148 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.history;
import org.apache.guacamole.net.auth.ConnectionRecordSet;
import org.apache.guacamole.net.basic.rest.APIError;
import org.apache.guacamole.net.basic.rest.APIException;
/**
* A sort predicate which species the property to use when sorting connection
* records, along with the sort order.
*
* @author Michael Jumper
*/
public class APIConnectionRecordSortPredicate {
/**
* The prefix which will be included before the name of a sortable property
* to indicate that the sort order is descending, not ascending.
*/
public static final String DESCENDING_PREFIX = "-";
/**
* All possible property name strings and their corresponding
* ConnectionRecordSet.SortableProperty values.
*/
public enum SortableProperty {
/**
* The date that the connection associated with the connection record
* began (connected).
*/
startDate(ConnectionRecordSet.SortableProperty.START_DATE);
/**
* The ConnectionRecordSet.SortableProperty that this property name
* string represents.
*/
public final ConnectionRecordSet.SortableProperty recordProperty;
/**
* Creates a new SortableProperty which associates the property name
* string (identical to its own name) with the given
* ConnectionRecordSet.SortableProperty value.
*
* @param recordProperty
* The ConnectionRecordSet.SortableProperty value to associate with
* the new SortableProperty.
*/
SortableProperty(ConnectionRecordSet.SortableProperty recordProperty) {
this.recordProperty = recordProperty;
}
}
/**
* The property to use when sorting ConnectionRecords.
*/
private ConnectionRecordSet.SortableProperty property;
/**
* Whether the requested sort order is descending (true) or ascending
* (false).
*/
private boolean descending;
/**
* Parses the given string value, determining the requested sort property
* and ordering. Possible values consist of any valid property name, and
* may include an optional prefix to denote descending sort order. Each
* possible property name is enumerated by the SortableValue enum.
*
* @param value
* The sort predicate string to parse, which must consist ONLY of a
* valid property name, possibly preceded by the DESCENDING_PREFIX.
*
* @throws APIException
* If the provided sort predicate string is invalid.
*/
public APIConnectionRecordSortPredicate(String value)
throws APIException {
// Parse whether sort order is descending
if (value.startsWith(DESCENDING_PREFIX)) {
descending = true;
value = value.substring(DESCENDING_PREFIX.length());
}
// Parse sorting property into ConnectionRecordSet.SortableProperty
try {
this.property = SortableProperty.valueOf(value).recordProperty;
}
// Bail out if sort property is not valid
catch (IllegalArgumentException e) {
throw new APIException(
APIError.Type.BAD_REQUEST,
String.format("Invalid sort property: \"%s\"", value)
);
}
}
/**
* Returns the SortableProperty defined by ConnectionRecordSet which
* represents the property requested.
*
* @return
* The ConnectionRecordSet.SortableProperty which refers to the same
* property as the string originally provided when this
* APIConnectionRecordSortPredicate was created.
*/
public ConnectionRecordSet.SortableProperty getProperty() {
return property;
}
/**
* Returns whether the requested sort order is descending.
*
* @return
* true if the sort order is descending, false if the sort order is
* ascending.
*/
public boolean isDescending() {
return descending;
}
}
@@ -0,0 +1,147 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.history;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.net.auth.ConnectionRecordSet;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.rest.ObjectRetrievalService;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A REST Service for retrieving and managing the history records of Guacamole
* objects.
*
* @author Michael Jumper
*/
@Path("/data/{dataSource}/history")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class HistoryRESTService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(HistoryRESTService.class);
/**
* The maximum number of history records to return in any one response.
*/
private static final int MAXIMUM_HISTORY_SIZE = 1000;
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/**
* Retrieves the usage history for all connections, restricted by optional
* filter parameters.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext containing the connection whose history is to be
* retrieved.
*
* @param requiredContents
* The set of strings that each must occur somewhere within the
* returned connection records, whether within the associated username,
* the name of the associated connection, or any associated date. If
* non-empty, any connection record not matching each of the strings
* within the collection will be excluded from the results.
*
* @param sortPredicates
* A list of predicates to apply while sorting the resulting connection
* records, describing the properties involved and the sort order for
* those properties.
*
* @return
* A list of connection records, describing the start and end times of
* various usages of this connection.
*
* @throws GuacamoleException
* If an error occurs while retrieving the connection history.
*/
@GET
@Path("/connections")
public List<APIConnectionRecord> getConnectionHistory(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@QueryParam("contains") List<String> requiredContents,
@QueryParam("order") List<APIConnectionRecordSortPredicate> sortPredicates)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Retrieve overall connection history
ConnectionRecordSet history = userContext.getConnectionHistory();
// Restrict to records which contain the specified strings
for (String required : requiredContents) {
if (!required.isEmpty())
history = history.contains(required);
}
// Sort according to specified ordering
for (APIConnectionRecordSortPredicate predicate : sortPredicates)
history = history.sort(predicate.getProperty(), predicate.isDescending());
// Limit to maximum result size
history = history.limit(MAXIMUM_HISTORY_SIZE);
// Convert record set to collection of API connection records
List<APIConnectionRecord> apiRecords = new ArrayList<APIConnectionRecord>();
for (ConnectionRecord record : history.asCollection())
apiRecords.add(new APIConnectionRecord(record));
// Return the converted history
return apiRecords;
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to retrieval or maintenance of history records using the
* Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest.history;
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.language;
import com.google.inject.Inject;
import java.util.Map;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.net.basic.extension.LanguageResourceService;
/**
* A REST Service for handling the listing of languages.
*
* @author James Muehlner
*/
@Path("/languages")
@Produces(MediaType.APPLICATION_JSON)
public class LanguageRESTService {
/**
* Service for retrieving information regarding available language
* resources.
*/
@Inject
private LanguageResourceService languageResourceService;
/**
* Returns a map of all available language keys to their corresponding
* human-readable names.
*
* @return
* A map of languages defined in the system, of language key to
* display name.
*/
@GET
public Map<String, String> getLanguages() {
return languageResourceService.getLanguageNames();
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the language retrieval aspect of the Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest.language;
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the basic Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest;
@@ -0,0 +1,133 @@
/*
* Copyright (C) 2016 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.patch;
import com.google.inject.Inject;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.net.basic.extension.PatchResourceService;
import org.apache.guacamole.net.basic.resource.Resource;
/**
* A REST Service for handling the listing of HTML patches.
*
* @author Michael Jumper
*/
@Path("/patches")
@Produces(MediaType.APPLICATION_JSON)
public class PatchRESTService {
/**
* Service for retrieving information regarding available HTML patch
* resources.
*/
@Inject
private PatchResourceService patchResourceService;
/**
* Reads the entire contents of the given resource as a String. The
* resource is assumed to be encoded in UTF-8.
*
* @param resource
* The resource to read as a new String.
*
* @return
* A new String containing the contents of the given resource.
*
* @throws IOException
* If an I/O error prevents reading the resource.
*/
private String readResourceAsString(Resource resource) throws IOException {
StringBuilder contents = new StringBuilder();
// Read entire resource into StringBuilder one chunk at a time
Reader reader = new InputStreamReader(resource.asStream(), "UTF-8");
try {
char buffer[] = new char[8192];
int length;
while ((length = reader.read(buffer)) != -1) {
contents.append(buffer, 0, length);
}
}
// Ensure resource is always closed
finally {
reader.close();
}
return contents.toString();
}
/**
* Returns a list of all available HTML patches, in the order they should
* be applied. Each patch is raw HTML containing additional meta tags
* describing how and where the patch should be applied.
*
* @return
* A list of all HTML patches defined in the system, in the order they
* should be applied.
*
* @throws GuacamoleException
* If an error occurs preventing any HTML patch from being read.
*/
@GET
public List<String> getPatches() throws GuacamoleException {
try {
// Allocate a list of equal size to the total number of patches
List<Resource> resources = patchResourceService.getPatchResources();
List<String> patches = new ArrayList<String>(resources.size());
// Convert each patch resource to a string
for (Resource resource : resources) {
patches.add(readResourceAsString(resource));
}
// Return all patches in string form
return patches;
}
// Bail out entirely on error
catch (IOException e) {
throw new GuacamoleServerException("Unable to read HTML patches.", e);
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2016 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the HTML patch retrieval aspect of the Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest.patch;
@@ -0,0 +1,300 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.permission;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
/**
* The set of permissions which are granted to a specific user, organized by
* object type and, if applicable, identifier. This object can be constructed
* with arbitrary permissions present, or manipulated after creation through
* the manipulation or replacement of its collections of permissions, but is
* otherwise not intended for internal use as a data structure for permissions.
* Its primary purpose is as a hierarchical format for exchanging granted
* permissions with REST clients.
*/
public class APIPermissionSet {
/**
* Map of connection ID to the set of granted permissions.
*/
private Map<String, Set<ObjectPermission.Type>> connectionPermissions =
new HashMap<String, Set<ObjectPermission.Type>>();
/**
* Map of connection group ID to the set of granted permissions.
*/
private Map<String, Set<ObjectPermission.Type>> connectionGroupPermissions =
new HashMap<String, Set<ObjectPermission.Type>>();
/**
* Map of active connection ID to the set of granted permissions.
*/
private Map<String, Set<ObjectPermission.Type>> activeConnectionPermissions =
new HashMap<String, Set<ObjectPermission.Type>>();
/**
* Map of user ID to the set of granted permissions.
*/
private Map<String, Set<ObjectPermission.Type>> userPermissions =
new HashMap<String, Set<ObjectPermission.Type>>();
/**
* Set of all granted system-level permissions.
*/
private Set<SystemPermission.Type> systemPermissions =
EnumSet.noneOf(SystemPermission.Type.class);
/**
* Creates a new permission set which contains no granted permissions. Any
* permissions must be added by manipulating or replacing the applicable
* permission collection.
*/
public APIPermissionSet() {
}
/**
* Adds the system permissions from the given SystemPermissionSet to the
* Set of system permissions provided.
*
* @param permissions
* The Set to add system permissions to.
*
* @param permSet
* The SystemPermissionSet containing the system permissions to add.
*
* @throws GuacamoleException
* If an error occurs while retrieving system permissions from the
* SystemPermissionSet.
*/
private void addSystemPermissions(Set<SystemPermission.Type> permissions,
SystemPermissionSet permSet) throws GuacamoleException {
// Add all provided system permissions
for (SystemPermission permission : permSet.getPermissions())
permissions.add(permission.getType());
}
/**
* Adds the object permissions from the given ObjectPermissionSet to the
* Map of object permissions provided.
*
* @param permissions
* The Map to add object permissions to.
*
* @param permSet
* The ObjectPermissionSet containing the object permissions to add.
*
* @throws GuacamoleException
* If an error occurs while retrieving object permissions from the
* ObjectPermissionSet.
*/
private void addObjectPermissions(Map<String, Set<ObjectPermission.Type>> permissions,
ObjectPermissionSet permSet) throws GuacamoleException {
// Add all provided object permissions
for (ObjectPermission permission : permSet.getPermissions()) {
// Get associated set of permissions
String identifier = permission.getObjectIdentifier();
Set<ObjectPermission.Type> objectPermissions = permissions.get(identifier);
// Create new set if none yet exists
if (objectPermissions == null)
permissions.put(identifier, EnumSet.of(permission.getType()));
// Otherwise add to existing set
else
objectPermissions.add(permission.getType());
}
}
/**
* Creates a new permission set containing all permissions currently
* granted to the given user.
*
* @param user
* The user whose permissions should be stored within this permission
* set.
*
* @throws GuacamoleException
* If an error occurs while retrieving the user's permissions.
*/
public APIPermissionSet(User user) throws GuacamoleException {
// Add all permissions from the provided user
addSystemPermissions(systemPermissions, user.getSystemPermissions());
addObjectPermissions(connectionPermissions, user.getConnectionPermissions());
addObjectPermissions(connectionGroupPermissions, user.getConnectionGroupPermissions());
addObjectPermissions(activeConnectionPermissions, user.getActiveConnectionPermissions());
addObjectPermissions(userPermissions, user.getUserPermissions());
}
/**
* Returns a map of connection IDs to the set of permissions granted for
* that connection. If no permissions are granted to a particular
* connection, its ID will not be present as a key in the map. This map is
* mutable, and changes to this map will affect the permission set
* directly.
*
* @return
* A map of connection IDs to the set of permissions granted for that
* connection.
*/
public Map<String, Set<ObjectPermission.Type>> getConnectionPermissions() {
return connectionPermissions;
}
/**
* Returns a map of connection group IDs to the set of permissions granted
* for that connection group. If no permissions are granted to a particular
* connection group, its ID will not be present as a key in the map. This
* map is mutable, and changes to this map will affect the permission set
* directly.
*
* @return
* A map of connection group IDs to the set of permissions granted for
* that connection group.
*/
public Map<String, Set<ObjectPermission.Type>> getConnectionGroupPermissions() {
return connectionGroupPermissions;
}
/**
* Returns a map of active connection IDs to the set of permissions granted
* for that active connection. If no permissions are granted to a particular
* active connection, its ID will not be present as a key in the map. This
* map is mutable, and changes to this map will affect the permission set
* directly.
*
* @return
* A map of active connection IDs to the set of permissions granted for
* that active connection.
*/
public Map<String, Set<ObjectPermission.Type>> getActiveConnectionPermissions() {
return activeConnectionPermissions;
}
/**
* Returns a map of user IDs to the set of permissions granted for that
* user. If no permissions are granted to a particular user, its ID will
* not be present as a key in the map. This map is mutable, and changes to
* to this map will affect the permission set directly.
*
* @return
* A map of user IDs to the set of permissions granted for that user.
*/
public Map<String, Set<ObjectPermission.Type>> getUserPermissions() {
return userPermissions;
}
/**
* Returns the set of granted system-level permissions. If no permissions
* are granted at the system level, this will be an empty set. This set is
* mutable, and changes to this set will affect the permission set
* directly.
*
* @return
* The set of granted system-level permissions.
*/
public Set<SystemPermission.Type> getSystemPermissions() {
return systemPermissions;
}
/**
* Replaces the current map of connection permissions with the given map,
* which must map connection ID to its corresponding set of granted
* permissions. If a connection has no permissions, its ID must not be
* present as a key in the map.
*
* @param connectionPermissions
* The map which must replace the currently-stored map of permissions.
*/
public void setConnectionPermissions(Map<String, Set<ObjectPermission.Type>> connectionPermissions) {
this.connectionPermissions = connectionPermissions;
}
/**
* Replaces the current map of connection group permissions with the given
* map, which must map connection group ID to its corresponding set of
* granted permissions. If a connection group has no permissions, its ID
* must not be present as a key in the map.
*
* @param connectionGroupPermissions
* The map which must replace the currently-stored map of permissions.
*/
public void setConnectionGroupPermissions(Map<String, Set<ObjectPermission.Type>> connectionGroupPermissions) {
this.connectionGroupPermissions = connectionGroupPermissions;
}
/**
* Replaces the current map of active connection permissions with the give
* map, which must map active connection ID to its corresponding set of
* granted permissions. If an active connection has no permissions, its ID
* must not be present as a key in the map.
*
* @param activeConnectionPermissions
* The map which must replace the currently-stored map of permissions.
*/
public void setActiveConnectionPermissions(Map<String, Set<ObjectPermission.Type>> activeConnectionPermissions) {
this.activeConnectionPermissions = activeConnectionPermissions;
}
/**
* Replaces the current map of user permissions with the given map, which
* must map user ID to its corresponding set of granted permissions. If a
* user has no permissions, its ID must not be present as a key in the map.
*
* @param userPermissions
* The map which must replace the currently-stored map of permissions.
*/
public void setUserPermissions(Map<String, Set<ObjectPermission.Type>> userPermissions) {
this.userPermissions = userPermissions;
}
/**
* Replaces the current set of system-level permissions with the given set.
* If no system-level permissions are granted, the empty set must be
* specified.
*
* @param systemPermissions
* The set which must replace the currently-stored set of permissions.
*/
public void setSystemPermissions(Set<SystemPermission.Type> systemPermissions) {
this.systemPermissions = systemPermissions;
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the permission manipulation aspect of the Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest.permission;
@@ -0,0 +1,199 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.schema;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.form.Form;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.rest.ObjectRetrievalService;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.apache.guacamole.protocols.ProtocolInfo;
/**
* A REST service which provides access to descriptions of the properties,
* attributes, etc. of objects used within the Guacamole REST API.
*
* @author Michael Jumper
*/
@Path("/schema/{dataSource}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class SchemaRESTService {
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/**
* Retrieves the possible attributes of a user object.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext dictating the available user attributes.
*
* @return
* A collection of forms which describe the possible attributes of a
* user object.
*
* @throws GuacamoleException
* If an error occurs while retrieving the possible attributes.
*/
@GET
@Path("/users/attributes")
public Collection<Form> getUserAttributes(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier)
throws GuacamoleException {
// Retrieve all possible user attributes
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
return userContext.getUserAttributes();
}
/**
* Retrieves the possible attributes of a connection object.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext dictating the available connection attributes.
*
* @return
* A collection of forms which describe the possible attributes of a
* connection object.
*
* @throws GuacamoleException
* If an error occurs while retrieving the possible attributes.
*/
@GET
@Path("/connections/attributes")
public Collection<Form> getConnectionAttributes(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier)
throws GuacamoleException {
// Retrieve all possible connection attributes
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
return userContext.getConnectionAttributes();
}
/**
* Retrieves the possible attributes of a connection group object.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext dictating the available connection group
* attributes.
*
* @return
* A collection of forms which describe the possible attributes of a
* connection group object.
*
* @throws GuacamoleException
* If an error occurs while retrieving the possible attributes.
*/
@GET
@Path("/connectionGroups/attributes")
public Collection<Form> getConnectionGroupAttributes(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier)
throws GuacamoleException {
// Retrieve all possible connection group attributes
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
return userContext.getConnectionGroupAttributes();
}
/**
* Gets a map of protocols defined in the system - protocol name to protocol.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext dictating the protocols available. Currently, the
* UserContext actually does not dictate this, the the same set of
* protocols will be retrieved for all users, though the identifier
* given here will be validated.
*
* @return
* A map of protocol information, where each key is the unique name
* associated with that protocol.
*
* @throws GuacamoleException
* If an error occurs while retrieving the available protocols.
*/
@GET
@Path("/protocols")
public Map<String, ProtocolInfo> getProtocols(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier)
throws GuacamoleException {
// Verify the given auth token and auth provider identifier are valid
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get and return a map of all protocols.
Environment env = new LocalEnvironment();
return env.getProtocols();
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the self-description of the Guacamole REST API, such as
* the attributes or parameters applicable to specific objects.
*/
package org.apache.guacamole.net.basic.rest.schema;
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.user;
import java.util.Map;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.apache.guacamole.net.auth.User;
/**
* A simple User to expose through the REST endpoints.
*
* @author James Muehlner
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class APIUser {
/**
* The username of this user.
*/
private String username;
/**
* The password of this user.
*/
private String password;
/**
* Map of all associated attributes by attribute identifier.
*/
private Map<String, String> attributes;
/**
* Construct a new empty APIUser.
*/
public APIUser() {}
/**
* Construct a new APIUser from the provided User.
* @param user The User to construct the APIUser from.
*/
public APIUser(User user) {
// Set user information
this.username = user.getIdentifier();
this.password = user.getPassword();
// Associate any attributes
this.attributes = user.getAttributes();
}
/**
* Returns the username for this user.
* @return The username for this user.
*/
public String getUsername() {
return username;
}
/**
* Set the username for this user.
* @param username The username for this user.
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Returns the password for this user.
* @return The password for this user.
*/
public String getPassword() {
return password;
}
/**
* Set the password for this user.
* @param password The password for this user.
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Returns a map of all attributes associated with this user. Each entry
* key is the attribute identifier, while each value is the attribute
* value itself.
*
* @return
* The attribute map for this user.
*/
public Map<String, String> getAttributes() {
return attributes;
}
/**
* Sets the map of all attributes associated with this user. Each entry key
* is the attribute identifier, while each value is the attribute value
* itself.
*
* @param attributes
* The attribute map for this user.
*/
public void setAttributes(Map<String, String> attributes) {
this.attributes = attributes;
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.user;
/**
* All the information necessary for the password update operation on a user.
*
* @author James Muehlner
*/
public class APIUserPasswordUpdate {
/**
* The old (current) password of this user.
*/
private String oldPassword;
/**
* The new password of this user.
*/
private String newPassword;
/**
* Returns the old password for this user. This password must match the
* user's current password for the password update operation to succeed.
*
* @return
* The old password for this user.
*/
public String getOldPassword() {
return oldPassword;
}
/**
* Set the old password for this user. This password must match the
* user's current password for the password update operation to succeed.
*
* @param oldPassword
* The old password for this user.
*/
public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword;
}
/**
* Returns the new password that will be assigned to this user.
*
* @return
* The new password for this user.
*/
public String getNewPassword() {
return newPassword;
}
/**
* Set the new password that will be assigned to this user.
*
* @param newPassword
* The new password for this user.
*/
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
}
@@ -0,0 +1,115 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.user;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleUnsupportedException;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
/**
* A wrapper to make an APIUser look like a User. Useful where an
* org.apache.guacamole.net.auth.User is required. As a simple wrapper for
* APIUser, access to permissions is not provided. Any attempt to access or
* manipulate permissions on an APIUserWrapper will result in an exception.
*
* @author James Muehlner
*/
public class APIUserWrapper implements User {
/**
* The wrapped APIUser.
*/
private final APIUser apiUser;
/**
* Wrap a given APIUser to expose as a User.
* @param apiUser The APIUser to wrap.
*/
public APIUserWrapper(APIUser apiUser) {
this.apiUser = apiUser;
}
@Override
public String getIdentifier() {
return apiUser.getUsername();
}
@Override
public void setIdentifier(String username) {
apiUser.setUsername(username);
}
@Override
public String getPassword() {
return apiUser.getPassword();
}
@Override
public void setPassword(String password) {
apiUser.setPassword(password);
}
@Override
public Map<String, String> getAttributes() {
return apiUser.getAttributes();
}
@Override
public void setAttributes(Map<String, String> attributes) {
apiUser.setAttributes(attributes);
}
@Override
public SystemPermissionSet getSystemPermissions()
throws GuacamoleException {
throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access.");
}
@Override
public ObjectPermissionSet getConnectionPermissions()
throws GuacamoleException {
throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access.");
}
@Override
public ObjectPermissionSet getConnectionGroupPermissions()
throws GuacamoleException {
throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access.");
}
@Override
public ObjectPermissionSet getUserPermissions()
throws GuacamoleException {
throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access.");
}
@Override
public ObjectPermissionSet getActiveConnectionPermissions()
throws GuacamoleException {
throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access.");
}
}
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.user;
import java.util.HashSet;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.permission.Permission;
import org.apache.guacamole.net.auth.permission.PermissionSet;
/**
* A set of changes to be applied to a PermissionSet, describing the set of
* permissions being added and removed.
*
* @author Michael Jumper
* @param <PermissionType>
* The type of permissions being added and removed.
*/
public class PermissionSetPatch<PermissionType extends Permission> {
/**
* The set of all permissions being added.
*/
private final Set<PermissionType> addedPermissions =
new HashSet<PermissionType>();
/**
* The set of all permissions being removed.
*/
private final Set<PermissionType> removedPermissions =
new HashSet<PermissionType>();
/**
* Queues the given permission to be added. The add operation will be
* performed only when apply() is called.
*
* @param permission
* The permission to add.
*/
public void addPermission(PermissionType permission) {
addedPermissions.add(permission);
}
/**
* Queues the given permission to be removed. The remove operation will be
* performed only when apply() is called.
*
* @param permission
* The permission to remove.
*/
public void removePermission(PermissionType permission) {
removedPermissions.add(permission);
}
/**
* Applies all queued changes to the given permission set.
*
* @param permissionSet
* The permission set to add and remove permissions from.
*
* @throws GuacamoleException
* If an error occurs while manipulating the permissions of the given
* permission set.
*/
public void apply(PermissionSet<PermissionType> permissionSet)
throws GuacamoleException {
// Add any added permissions
if (!addedPermissions.isEmpty())
permissionSet.addPermissions(addedPermissions);
// Remove any removed permissions
if (!removedPermissions.isEmpty())
permissionSet.removePermissions(removedPermissions);
}
}
@@ -0,0 +1,647 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.rest.user;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.auth.permission.Permission;
import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
import org.apache.guacamole.net.basic.GuacamoleSession;
import org.apache.guacamole.net.basic.rest.APIPatch;
import static org.apache.guacamole.net.basic.rest.APIPatch.Operation.add;
import static org.apache.guacamole.net.basic.rest.APIPatch.Operation.remove;
import org.apache.guacamole.net.basic.rest.ObjectRetrievalService;
import org.apache.guacamole.net.basic.rest.PATCH;
import org.apache.guacamole.net.basic.rest.auth.AuthenticationService;
import org.apache.guacamole.net.basic.rest.permission.APIPermissionSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A REST Service for handling user CRUD operations.
*
* @author James Muehlner
* @author Michael Jumper
*/
@Path("/data/{dataSource}/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserRESTService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(UserRESTService.class);
/**
* The prefix of any path within an operation of a JSON patch which
* modifies the permissions of a user regarding a specific connection.
*/
private static final String CONNECTION_PERMISSION_PATCH_PATH_PREFIX = "/connectionPermissions/";
/**
* The prefix of any path within an operation of a JSON patch which
* modifies the permissions of a user regarding a specific connection group.
*/
private static final String CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX = "/connectionGroupPermissions/";
/**
* The prefix of any path within an operation of a JSON patch which
* modifies the permissions of a user regarding a specific active connection.
*/
private static final String ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX = "/activeConnectionPermissions/";
/**
* The prefix of any path within an operation of a JSON patch which
* modifies the permissions of a user regarding another, specific user.
*/
private static final String USER_PERMISSION_PATCH_PATH_PREFIX = "/userPermissions/";
/**
* The path of any operation within a JSON patch which modifies the
* permissions of a user regarding the entire system.
*/
private static final String SYSTEM_PERMISSION_PATCH_PATH = "/systemPermissions";
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* Service for convenient retrieval of objects.
*/
@Inject
private ObjectRetrievalService retrievalService;
/**
* Gets a list of users in the given data source (UserContext), filtering
* the returned list by the given permission, if specified.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext from which the users are to be retrieved.
*
* @param permissions
* The set of permissions to filter with. A user must have one or more
* of these permissions for a user to appear in the result.
* If null, no filtering will be performed.
*
* @return
* A list of all visible users. If a permission was specified, this
* list will contain only those users for whom the current user has
* that permission.
*
* @throws GuacamoleException
* If an error is encountered while retrieving users.
*/
@GET
public List<APIUser> getUsers(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@QueryParam("permission") List<ObjectPermission.Type> permissions)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// An admin user has access to any user
User self = userContext.self();
SystemPermissionSet systemPermissions = self.getSystemPermissions();
boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER);
// Get the directory
Directory<User> userDirectory = userContext.getUserDirectory();
// Filter users, if requested
Collection<String> userIdentifiers = userDirectory.getIdentifiers();
if (!isAdmin && permissions != null && !permissions.isEmpty()) {
ObjectPermissionSet userPermissions = self.getUserPermissions();
userIdentifiers = userPermissions.getAccessibleObjects(permissions, userIdentifiers);
}
// Retrieve all users, converting to API users
List<APIUser> apiUsers = new ArrayList<APIUser>();
for (User user : userDirectory.getAll(userIdentifiers))
apiUsers.add(new APIUser(user));
return apiUsers;
}
/**
* Retrieves an individual user.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext from which the requested user is to be retrieved.
*
* @param username
* The username of the user to retrieve.
*
* @return user
* The user having the given username.
*
* @throws GuacamoleException
* If an error occurs while retrieving the user.
*/
@GET
@Path("/{username}")
public APIUser getUser(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
// Retrieve the requested user
User user = retrievalService.retrieveUser(session, authProviderIdentifier, username);
return new APIUser(user);
}
/**
* Creates a new user and returns the user that was created.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext in which the requested user is to be created.
*
* @param user
* The new user to create.
*
* @throws GuacamoleException
* If a problem is encountered while creating the user.
*
* @return
* The newly created user.
*/
@POST
public APIUser createUser(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier, APIUser user)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the directory
Directory<User> userDirectory = userContext.getUserDirectory();
// Randomly set the password if it wasn't provided
if (user.getPassword() == null)
user.setPassword(UUID.randomUUID().toString());
// Create the user
userDirectory.add(new APIUserWrapper(user));
return user;
}
/**
* Updates an individual existing user.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext in which the requested user is to be updated.
*
* @param username
* The username of the user to update.
*
* @param user
* The data to update the user with.
*
* @throws GuacamoleException
* If an error occurs while updating the user.
*/
@PUT
@Path("/{username}")
public void updateUser(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username, APIUser user)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the directory
Directory<User> userDirectory = userContext.getUserDirectory();
// Validate data and path are sane
if (!user.getUsername().equals(username))
throw new GuacamoleClientException("Username in path does not match username provided JSON data.");
// A user may not use this endpoint to modify himself
if (userContext.self().getIdentifier().equals(user.getUsername()))
throw new GuacamoleSecurityException("Permission denied.");
// Get the user
User existingUser = retrievalService.retrieveUser(userContext, username);
// Do not update the user password if no password was provided
if (user.getPassword() != null)
existingUser.setPassword(user.getPassword());
// Update user attributes
existingUser.setAttributes(user.getAttributes());
// Update the user
userDirectory.update(existingUser);
}
/**
* Updates the password for an individual existing user.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext in which the requested user is to be updated.
*
* @param username
* The username of the user to update.
*
* @param userPasswordUpdate
* The object containing the old password for the user, as well as the
* new password to set for that user.
*
* @param request
* The HttpServletRequest associated with the password update attempt.
*
* @throws GuacamoleException
* If an error occurs while updating the user's password.
*/
@PUT
@Path("/{username}/password")
public void updatePassword(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username,
APIUserPasswordUpdate userPasswordUpdate,
@Context HttpServletRequest request) throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Build credentials
Credentials credentials = new Credentials();
credentials.setUsername(username);
credentials.setPassword(userPasswordUpdate.getOldPassword());
credentials.setRequest(request);
credentials.setSession(request.getSession(true));
// Verify that the old password was correct
try {
AuthenticationProvider authProvider = userContext.getAuthenticationProvider();
if (authProvider.authenticateUser(credentials) == null)
throw new GuacamoleSecurityException("Permission denied.");
}
// Pass through any credentials exceptions as simple permission denied
catch (GuacamoleCredentialsException e) {
throw new GuacamoleSecurityException("Permission denied.");
}
// Get the user directory
Directory<User> userDirectory = userContext.getUserDirectory();
// Get the user that we want to updates
User user = retrievalService.retrieveUser(userContext, username);
// Set password to the newly provided one
user.setPassword(userPasswordUpdate.getNewPassword());
// Update the user
userDirectory.update(user);
}
/**
* Deletes an individual existing user.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext from which the requested user is to be deleted.
*
* @param username
* The username of the user to delete.
*
* @throws GuacamoleException
* If an error occurs while deleting the user.
*/
@DELETE
@Path("/{username}")
public void deleteUser(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the directory
Directory<User> userDirectory = userContext.getUserDirectory();
// Get the user
User existingUser = userDirectory.get(username);
if (existingUser == null)
throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\"");
// Delete the user
userDirectory.remove(username);
}
/**
* Gets a list of permissions for the user with the given username.
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext in which the requested user is to be found.
*
* @param username
* The username of the user to retrieve permissions for.
*
* @return
* A list of all permissions granted to the specified user.
*
* @throws GuacamoleException
* If an error occurs while retrieving permissions.
*/
@GET
@Path("/{username}/permissions")
public APIPermissionSet getPermissions(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username)
throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
User user;
// If username is own username, just use self - might not have query permissions
if (userContext.self().getIdentifier().equals(username))
user = userContext.self();
// If not self, query corresponding user from directory
else {
user = userContext.getUserDirectory().get(username);
if (user == null)
throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\"");
}
return new APIPermissionSet(user);
}
/**
* Updates the given permission set patch by queuing an add or remove
* operation for the given permission based on the given patch operation.
*
* @param <PermissionType>
* The type of permission stored within the permission set.
*
* @param operation
* The patch operation to perform.
*
* @param permissionSetPatch
* The permission set patch being modified.
*
* @param permission
* The permission being added or removed from the set.
*
* @throws GuacamoleException
* If the requested patch operation is not supported.
*/
private <PermissionType extends Permission> void updatePermissionSet(
APIPatch.Operation operation,
PermissionSetPatch<PermissionType> permissionSetPatch,
PermissionType permission) throws GuacamoleException {
// Add or remove permission based on operation
switch (operation) {
// Add permission
case add:
permissionSetPatch.addPermission(permission);
break;
// Remove permission
case remove:
permissionSetPatch.removePermission(permission);
break;
// Unsupported patch operation
default:
throw new GuacamoleClientException("Unsupported patch operation: \"" + operation + "\"");
}
}
/**
* Applies a given list of permission patches. Each patch specifies either
* an "add" or a "remove" operation for a permission type, represented by
* a string. Valid permission types depend on the path of each patch
* operation, as the path dictates the permission being modified, such as
* "/connectionPermissions/42" or "/systemPermissions".
*
* @param authToken
* The authentication token that is used to authenticate the user
* performing the operation.
*
* @param authProviderIdentifier
* The unique identifier of the AuthenticationProvider associated with
* the UserContext in which the requested user is to be found.
*
* @param username
* The username of the user to modify the permissions of.
*
* @param patches
* The permission patches to apply for this request.
*
* @throws GuacamoleException
* If a problem is encountered while modifying permissions.
*/
@PATCH
@Path("/{username}/permissions")
public void patchPermissions(@QueryParam("token") String authToken,
@PathParam("dataSource") String authProviderIdentifier,
@PathParam("username") String username,
List<APIPatch<String>> patches) throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier);
// Get the user
User user = userContext.getUserDirectory().get(username);
if (user == null)
throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\"");
// Permission patches for all types of permissions
PermissionSetPatch<ObjectPermission> connectionPermissionPatch = new PermissionSetPatch<ObjectPermission>();
PermissionSetPatch<ObjectPermission> connectionGroupPermissionPatch = new PermissionSetPatch<ObjectPermission>();
PermissionSetPatch<ObjectPermission> activeConnectionPermissionPatch = new PermissionSetPatch<ObjectPermission>();
PermissionSetPatch<ObjectPermission> userPermissionPatch = new PermissionSetPatch<ObjectPermission>();
PermissionSetPatch<SystemPermission> systemPermissionPatch = new PermissionSetPatch<SystemPermission>();
// Apply all patch operations individually
for (APIPatch<String> patch : patches) {
String path = patch.getPath();
// Create connection permission if path has connection prefix
if (path.startsWith(CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) {
// Get identifier and type from patch operation
String identifier = path.substring(CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length());
ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue());
// Create and update corresponding permission
ObjectPermission permission = new ObjectPermission(type, identifier);
updatePermissionSet(patch.getOp(), connectionPermissionPatch, permission);
}
// Create connection group permission if path has connection group prefix
else if (path.startsWith(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX)) {
// Get identifier and type from patch operation
String identifier = path.substring(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX.length());
ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue());
// Create and update corresponding permission
ObjectPermission permission = new ObjectPermission(type, identifier);
updatePermissionSet(patch.getOp(), connectionGroupPermissionPatch, permission);
}
// Create active connection permission if path has active connection prefix
else if (path.startsWith(ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) {
// Get identifier and type from patch operation
String identifier = path.substring(ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length());
ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue());
// Create and update corresponding permission
ObjectPermission permission = new ObjectPermission(type, identifier);
updatePermissionSet(patch.getOp(), activeConnectionPermissionPatch, permission);
}
// Create user permission if path has user prefix
else if (path.startsWith(USER_PERMISSION_PATCH_PATH_PREFIX)) {
// Get identifier and type from patch operation
String identifier = path.substring(USER_PERMISSION_PATCH_PATH_PREFIX.length());
ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue());
// Create and update corresponding permission
ObjectPermission permission = new ObjectPermission(type, identifier);
updatePermissionSet(patch.getOp(), userPermissionPatch, permission);
}
// Create system permission if path is system path
else if (path.equals(SYSTEM_PERMISSION_PATCH_PATH)) {
// Get identifier and type from patch operation
SystemPermission.Type type = SystemPermission.Type.valueOf(patch.getValue());
// Create and update corresponding permission
SystemPermission permission = new SystemPermission(type);
updatePermissionSet(patch.getOp(), systemPermissionPatch, permission);
}
// Otherwise, the path is not supported
else
throw new GuacamoleClientException("Unsupported patch path: \"" + path + "\"");
} // end for each patch operation
// Save the permission changes
connectionPermissionPatch.apply(user.getConnectionPermissions());
connectionGroupPermissionPatch.apply(user.getConnectionGroupPermissions());
activeConnectionPermissionPatch.apply(user.getActiveConnectionPermissions());
userPermissionPatch.apply(user.getUserPermissions());
systemPermissionPatch.apply(user.getSystemPermissions());
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes related to the user manipulation aspect of the Guacamole REST API.
*/
package org.apache.guacamole.net.basic.rest.user;
@@ -0,0 +1,120 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket;
import com.google.inject.Provider;
import java.util.Map;
import javax.websocket.EndpointConfig;
import javax.websocket.HandshakeResponse;
import javax.websocket.Session;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.basic.TunnelRequest;
import org.apache.guacamole.net.basic.TunnelRequestService;
import org.apache.guacamole.websocket.GuacamoleWebSocketTunnelEndpoint;
/**
* Tunnel implementation which uses WebSocket as a tunnel backend, rather than
* HTTP, properly parsing connection IDs included in the connection request.
*/
public class BasicGuacamoleWebSocketTunnelEndpoint extends GuacamoleWebSocketTunnelEndpoint {
/**
* Unique string which shall be used to store the TunnelRequest
* associated with a WebSocket connection.
*/
private static final String TUNNEL_REQUEST_PROPERTY = "WS_GUAC_TUNNEL_REQUEST";
/**
* Unique string which shall be used to store the TunnelRequestService to
* be used for processing TunnelRequests.
*/
private static final String TUNNEL_REQUEST_SERVICE_PROPERTY = "WS_GUAC_TUNNEL_REQUEST_SERVICE";
/**
* Configurator implementation which stores the requested GuacamoleTunnel
* within the user properties. The GuacamoleTunnel will be later retrieved
* during the connection process.
*/
public static class Configurator extends ServerEndpointConfig.Configurator {
/**
* Provider which provides instances of a service for handling
* tunnel requests.
*/
private final Provider<TunnelRequestService> tunnelRequestServiceProvider;
/**
* Creates a new Configurator which uses the given tunnel request
* service provider to retrieve the necessary service to handle new
* connections requests.
*
* @param tunnelRequestServiceProvider
* The tunnel request service provider to use for all new
* connections.
*/
public Configurator(Provider<TunnelRequestService> tunnelRequestServiceProvider) {
this.tunnelRequestServiceProvider = tunnelRequestServiceProvider;
}
@Override
public void modifyHandshake(ServerEndpointConfig config,
HandshakeRequest request, HandshakeResponse response) {
super.modifyHandshake(config, request, response);
// Store tunnel request and tunnel request service for retrieval
// upon WebSocket open
Map<String, Object> userProperties = config.getUserProperties();
userProperties.clear();
userProperties.put(TUNNEL_REQUEST_PROPERTY, new WebSocketTunnelRequest(request));
userProperties.put(TUNNEL_REQUEST_SERVICE_PROPERTY, tunnelRequestServiceProvider.get());
}
}
@Override
protected GuacamoleTunnel createTunnel(Session session,
EndpointConfig config) throws GuacamoleException {
Map<String, Object> userProperties = config.getUserProperties();
// Get original tunnel request
TunnelRequest tunnelRequest = (TunnelRequest) userProperties.get(TUNNEL_REQUEST_PROPERTY);
if (tunnelRequest == null)
return null;
// Get tunnel request service
TunnelRequestService tunnelRequestService = (TunnelRequestService) userProperties.get(TUNNEL_REQUEST_SERVICE_PROPERTY);
if (tunnelRequestService == null)
return null;
// Create and return tunnel
return tunnelRequestService.createTunnel(tunnelRequest);
}
}
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket;
import com.google.inject.Provider;
import com.google.inject.servlet.ServletModule;
import java.util.Arrays;
import javax.websocket.DeploymentException;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import org.apache.guacamole.net.basic.TunnelLoader;
import org.apache.guacamole.net.basic.TunnelRequestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Loads the JSR-356 WebSocket tunnel implementation.
*
* @author Michael Jumper
*/
public class WebSocketTunnelModule extends ServletModule implements TunnelLoader {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(WebSocketTunnelModule.class);
@Override
public boolean isSupported() {
try {
// Attempt to find WebSocket servlet
Class.forName("javax.websocket.Endpoint");
// Support found
return true;
}
// If no such servlet class, this particular WebSocket support
// is not present
catch (ClassNotFoundException e) {}
catch (NoClassDefFoundError e) {}
// Support not found
return false;
}
@Override
public void configureServlets() {
logger.info("Loading JSR-356 WebSocket support...");
// Get container
ServerContainer container = (ServerContainer) getServletContext().getAttribute("javax.websocket.server.ServerContainer");
if (container == null) {
logger.warn("ServerContainer attribute required by JSR-356 is missing. Cannot load JSR-356 WebSocket support.");
return;
}
Provider<TunnelRequestService> tunnelRequestServiceProvider = getProvider(TunnelRequestService.class);
// Build configuration for WebSocket tunnel
ServerEndpointConfig config =
ServerEndpointConfig.Builder.create(BasicGuacamoleWebSocketTunnelEndpoint.class, "/websocket-tunnel")
.configurator(new BasicGuacamoleWebSocketTunnelEndpoint.Configurator(tunnelRequestServiceProvider))
.subprotocols(Arrays.asList(new String[]{"guacamole"}))
.build();
try {
// Add configuration to container
container.addEndpoint(config);
}
catch (DeploymentException e) {
logger.error("Unable to deploy WebSocket tunnel.", e);
}
}
}
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket;
import java.util.List;
import java.util.Map;
import javax.websocket.server.HandshakeRequest;
import org.apache.guacamole.net.basic.TunnelRequest;
/**
* WebSocket-specific implementation of TunnelRequest.
*
* @author Michael Jumper
*/
public class WebSocketTunnelRequest extends TunnelRequest {
/**
* All parameters passed via HTTP to the WebSocket handshake.
*/
private final Map<String, List<String>> handshakeParameters;
/**
* Creates a TunnelRequest implementation which delegates parameter and
* session retrieval to the given HandshakeRequest.
*
* @param request The HandshakeRequest to wrap.
*/
public WebSocketTunnelRequest(HandshakeRequest request) {
this.handshakeParameters = request.getParameterMap();
}
@Override
public String getParameter(String name) {
// Pull list of values, if present
List<String> values = getParameterValues(name);
if (values == null || values.isEmpty())
return null;
// Return first parameter value arbitrarily
return values.get(0);
}
@Override
public List<String> getParameterValues(String name) {
return handshakeParameters.get(name);
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty8;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.basic.TunnelRequestService;
import org.apache.guacamole.net.basic.TunnelRequest;
/**
* Tunnel servlet implementation which uses WebSocket as a tunnel backend,
* rather than HTTP, properly parsing connection IDs included in the connection
* request.
*/
@Singleton
public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet {
/**
* Service for handling tunnel requests.
*/
@Inject
private TunnelRequestService tunnelRequestService;
@Override
protected GuacamoleTunnel doConnect(TunnelRequest request)
throws GuacamoleException {
return tunnelRequestService.createTunnel(request);
}
}
@@ -0,0 +1,232 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty8;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.io.GuacamoleReader;
import org.apache.guacamole.io.GuacamoleWriter;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocket.Connection;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleConnectionClosedException;
import org.apache.guacamole.net.basic.HTTPTunnelRequest;
import org.apache.guacamole.net.basic.TunnelRequest;
import org.apache.guacamole.protocol.GuacamoleStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet.
*
* @author Michael Jumper
*/
public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelServlet.class);
/**
* The default, minimum buffer size for instructions.
*/
private static final int BUFFER_SIZE = 8192;
/**
* Sends the given status on the given WebSocket connection and closes the
* connection.
*
* @param connection The WebSocket connection to close.
* @param guac_status The status to send.
*/
public static void closeConnection(Connection connection,
GuacamoleStatus guac_status) {
connection.close(guac_status.getWebSocketCode(),
Integer.toString(guac_status.getGuacamoleStatusCode()));
}
@Override
public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
final TunnelRequest tunnelRequest = new HTTPTunnelRequest(request);
// Return new WebSocket which communicates through tunnel
return new WebSocket.OnTextMessage() {
/**
* The GuacamoleTunnel associated with the connected WebSocket. If
* the WebSocket has not yet been connected, this will be null.
*/
private GuacamoleTunnel tunnel = null;
@Override
public void onMessage(String string) {
// Ignore inbound messages if there is no associated tunnel
if (tunnel == null)
return;
GuacamoleWriter writer = tunnel.acquireWriter();
// Write message received
try {
writer.write(string.toCharArray());
}
catch (GuacamoleConnectionClosedException e) {
logger.debug("Connection to guacd closed.", e);
}
catch (GuacamoleException e) {
logger.debug("WebSocket tunnel write failed.", e);
}
tunnel.releaseWriter();
}
@Override
public void onOpen(final Connection connection) {
try {
tunnel = doConnect(tunnelRequest);
}
catch (GuacamoleException e) {
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage());
logger.debug("Error connecting WebSocket tunnel.", e);
closeConnection(connection, e.getStatus());
return;
}
// Do not start connection if tunnel does not exist
if (tunnel == null) {
closeConnection(connection, GuacamoleStatus.RESOURCE_NOT_FOUND);
return;
}
Thread readThread = new Thread() {
@Override
public void run() {
StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
GuacamoleReader reader = tunnel.acquireReader();
char[] readMessage;
try {
try {
// Attempt to read
while ((readMessage = reader.read()) != null) {
// Buffer message
buffer.append(readMessage);
// Flush if we expect to wait or buffer is getting full
if (!reader.available() || buffer.length() >= BUFFER_SIZE) {
connection.sendMessage(buffer.toString());
buffer.setLength(0);
}
}
// No more data
closeConnection(connection, GuacamoleStatus.SUCCESS);
}
// Catch any thrown guacamole exception and attempt
// 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);
closeConnection(connection, e.getStatus());
}
catch (GuacamoleConnectionClosedException e) {
logger.debug("Connection to guacd closed.", e);
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);
closeConnection(connection, e.getStatus());
}
}
catch (IOException e) {
logger.debug("WebSocket tunnel read failed due to I/O error.", e);
}
}
};
readThread.start();
}
@Override
public void onClose(int i, String string) {
try {
if (tunnel != null)
tunnel.close();
}
catch (GuacamoleException e) {
logger.debug("Unable to close connection to guacd.", e);
}
}
};
}
/**
* Called whenever the JavaScript Guacamole client makes a connection
* request. It it up to the implementor of this function to define what
* conditions must be met for a tunnel to be configured and returned as a
* result of this connection request (whether some sort of credentials must
* be specified, for example).
*
* @param request
* The TunnelRequest associated with the connection request received.
* Any parameters specified along with the connection request can be
* read from this object.
*
* @return
* A newly constructed GuacamoleTunnel if successful, null otherwise.
*
* @throws GuacamoleException
* If an error occurs while constructing the GuacamoleTunnel, or if the
* conditions required for connection are not met.
*/
protected abstract GuacamoleTunnel doConnect(TunnelRequest request)
throws GuacamoleException;
}
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty8;
import com.google.inject.servlet.ServletModule;
import org.apache.guacamole.net.basic.TunnelLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Loads the Jetty 8 WebSocket tunnel implementation.
*
* @author Michael Jumper
*/
public class WebSocketTunnelModule extends ServletModule implements TunnelLoader {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(WebSocketTunnelModule.class);
@Override
public boolean isSupported() {
try {
// Attempt to find WebSocket servlet
Class.forName("org.apache.guacamole.net.basic.websocket.jetty8.BasicGuacamoleWebSocketTunnelServlet");
// Support found
return true;
}
// If no such servlet class, this particular WebSocket support
// is not present
catch (ClassNotFoundException e) {}
catch (NoClassDefFoundError e) {}
// Support not found
return false;
}
@Override
public void configureServlets() {
logger.info("Loading Jetty 8 WebSocket support...");
serve("/websocket-tunnel").with(BasicGuacamoleWebSocketTunnelServlet.class);
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Jetty 8 WebSocket tunnel implementation. The classes here require Jetty 8.
*/
package org.apache.guacamole.net.basic.websocket.jetty8;
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty9;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.apache.guacamole.net.basic.TunnelRequestService;
/**
* WebSocketCreator which selects the appropriate WebSocketListener
* implementation if the "guacamole" subprotocol is in use.
*
* @author Michael Jumper
*/
public class BasicGuacamoleWebSocketCreator implements WebSocketCreator {
/**
* Service for handling tunnel requests.
*/
private final TunnelRequestService tunnelRequestService;
/**
* Creates a new WebSocketCreator which uses the given TunnelRequestService
* to create new GuacamoleTunnels for inbound requests.
*
* @param tunnelRequestService The service to use for inbound tunnel
* requests.
*/
public BasicGuacamoleWebSocketCreator(TunnelRequestService tunnelRequestService) {
this.tunnelRequestService = tunnelRequestService;
}
@Override
public Object createWebSocket(UpgradeRequest request, UpgradeResponse response) {
// Validate and use "guacamole" subprotocol
for (String subprotocol : request.getSubProtocols()) {
if ("guacamole".equals(subprotocol)) {
response.setAcceptedSubProtocol(subprotocol);
return new BasicGuacamoleWebSocketTunnelListener(tunnelRequestService);
}
}
// Invalid protocol
return null;
}
}
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty9;
import org.eclipse.jetty.websocket.api.Session;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.basic.TunnelRequestService;
/**
* WebSocket listener implementation which properly parses connection IDs
* included in the connection request.
*
* @author Michael Jumper
*/
public class BasicGuacamoleWebSocketTunnelListener extends GuacamoleWebSocketTunnelListener {
/**
* Service for handling tunnel requests.
*/
private final TunnelRequestService tunnelRequestService;
/**
* Creates a new WebSocketListener which uses the given TunnelRequestService
* to create new GuacamoleTunnels for inbound requests.
*
* @param tunnelRequestService The service to use for inbound tunnel
* requests.
*/
public BasicGuacamoleWebSocketTunnelListener(TunnelRequestService tunnelRequestService) {
this.tunnelRequestService = tunnelRequestService;
}
@Override
protected GuacamoleTunnel createTunnel(Session session) throws GuacamoleException {
return tunnelRequestService.createTunnel(new WebSocketTunnelRequest(session.getUpgradeRequest()));
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty9;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.apache.guacamole.net.basic.TunnelRequestService;
/**
* A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet.
*
* @author Michael Jumper
*/
@Singleton
public class BasicGuacamoleWebSocketTunnelServlet extends WebSocketServlet {
/**
* Service for handling tunnel requests.
*/
@Inject
private TunnelRequestService tunnelRequestService;
@Override
public void configure(WebSocketServletFactory factory) {
// Register WebSocket implementation
factory.setCreator(new BasicGuacamoleWebSocketCreator(tunnelRequestService));
}
}
@@ -0,0 +1,243 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty9;
import java.io.IOException;
import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleConnectionClosedException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.io.GuacamoleReader;
import org.apache.guacamole.io.GuacamoleWriter;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.protocol.GuacamoleStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* WebSocket listener implementation which provides a Guacamole tunnel
*
* @author Michael Jumper
*/
public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListener {
/**
* The default, minimum buffer size for instructions.
*/
private static final int BUFFER_SIZE = 8192;
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(BasicGuacamoleWebSocketTunnelServlet.class);
/**
* The underlying GuacamoleTunnel. WebSocket reads/writes will be handled
* as reads/writes to this tunnel.
*/
private GuacamoleTunnel tunnel;
/**
* Sends the given status on the given WebSocket connection and closes the
* connection.
*
* @param session The outbound WebSocket connection to close.
* @param guac_status The status to send.
*/
private void closeConnection(Session session, GuacamoleStatus guac_status) {
try {
int code = guac_status.getWebSocketCode();
String message = Integer.toString(guac_status.getGuacamoleStatusCode());
session.close(new CloseStatus(code, message));
}
catch (IOException e) {
logger.debug("Unable to close WebSocket connection.", e);
}
}
/**
* Returns a new tunnel for the given session. How this tunnel is created
* or retrieved is implementation-dependent.
*
* @param session The session associated with the active WebSocket
* connection.
* @return A connected tunnel, or null if no such tunnel exists.
* @throws GuacamoleException If an error occurs while retrieving the
* tunnel, or if access to the tunnel is denied.
*/
protected abstract GuacamoleTunnel createTunnel(Session session)
throws GuacamoleException;
@Override
public void onWebSocketConnect(final Session session) {
try {
// Get tunnel
tunnel = createTunnel(session);
if (tunnel == null) {
closeConnection(session, GuacamoleStatus.RESOURCE_NOT_FOUND);
return;
}
}
catch (GuacamoleException e) {
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage());
logger.debug("Error connecting WebSocket tunnel.", e);
closeConnection(session, e.getStatus());
return;
}
// Prepare read transfer thread
Thread readThread = new Thread() {
/**
* Remote (client) side of this connection
*/
private final RemoteEndpoint remote = session.getRemote();
@Override
public void run() {
StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
GuacamoleReader reader = tunnel.acquireReader();
char[] readMessage;
try {
try {
// Attempt to read
while ((readMessage = reader.read()) != null) {
// Buffer message
buffer.append(readMessage);
// Flush if we expect to wait or buffer is getting full
if (!reader.available() || buffer.length() >= BUFFER_SIZE) {
remote.sendString(buffer.toString());
buffer.setLength(0);
}
}
// No more data
closeConnection(session, GuacamoleStatus.SUCCESS);
}
// Catch any thrown guacamole exception and attempt
// 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);
closeConnection(session, e.getStatus());
}
catch (GuacamoleConnectionClosedException e) {
logger.debug("Connection to guacd closed.", e);
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);
closeConnection(session, e.getStatus());
}
}
catch (IOException e) {
logger.debug("I/O error prevents further reads.", e);
}
}
};
readThread.start();
}
@Override
public void onWebSocketText(String message) {
// Ignore inbound messages if there is no associated tunnel
if (tunnel == null)
return;
GuacamoleWriter writer = tunnel.acquireWriter();
try {
// Write received message
writer.write(message.toCharArray());
}
catch (GuacamoleConnectionClosedException e) {
logger.debug("Connection to guacd closed.", e);
}
catch (GuacamoleException e) {
logger.debug("WebSocket tunnel write failed.", e);
}
tunnel.releaseWriter();
}
@Override
public void onWebSocketBinary(byte[] payload, int offset, int length) {
throw new UnsupportedOperationException("Binary WebSocket messages are not supported.");
}
@Override
public void onWebSocketError(Throwable t) {
logger.debug("WebSocket tunnel closing due to error.", t);
try {
if (tunnel != null)
tunnel.close();
}
catch (GuacamoleException e) {
logger.debug("Unable to close connection to guacd.", e);
}
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
try {
if (tunnel != null)
tunnel.close();
}
catch (GuacamoleException e) {
logger.debug("Unable to close connection to guacd.", e);
}
}
}
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty9;
import com.google.inject.servlet.ServletModule;
import org.apache.guacamole.net.basic.TunnelLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Loads the Jetty 9 WebSocket tunnel implementation.
*
* @author Michael Jumper
*/
public class WebSocketTunnelModule extends ServletModule implements TunnelLoader {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(WebSocketTunnelModule.class);
@Override
public boolean isSupported() {
try {
// Attempt to find WebSocket servlet
Class.forName("org.apache.guacamole.net.basic.websocket.jetty9.BasicGuacamoleWebSocketTunnelServlet");
// Support found
return true;
}
// If no such servlet class, this particular WebSocket support
// is not present
catch (ClassNotFoundException e) {}
catch (NoClassDefFoundError e) {}
// Support not found
return false;
}
@Override
public void configureServlets() {
logger.info("Loading Jetty 9 WebSocket support...");
serve("/websocket-tunnel").with(BasicGuacamoleWebSocketTunnelServlet.class);
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.jetty9;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.apache.guacamole.net.basic.TunnelRequest;
/**
* Jetty 9 WebSocket-specific implementation of TunnelRequest.
*
* @author Michael Jumper
*/
public class WebSocketTunnelRequest extends TunnelRequest {
/**
* All parameters passed via HTTP to the WebSocket handshake.
*/
private final Map<String, String[]> handshakeParameters;
/**
* Creates a TunnelRequest implementation which delegates parameter and
* session retrieval to the given UpgradeRequest.
*
* @param request The UpgradeRequest to wrap.
*/
public WebSocketTunnelRequest(UpgradeRequest request) {
this.handshakeParameters = request.getParameterMap();
}
@Override
public String getParameter(String name) {
// Pull list of values, if present
List<String> values = getParameterValues(name);
if (values == null || values.isEmpty())
return null;
// Return first parameter value arbitrarily
return values.get(0);
}
@Override
public List<String> getParameterValues(String name) {
String[] values = handshakeParameters.get(name);
if (values == null)
return null;
return Arrays.asList(values);
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Jetty 9 WebSocket tunnel implementation. The classes here require at least
* Jetty 9, prior to Jetty 9.1 (when support for JSR 356 was implemented).
*/
package org.apache.guacamole.net.basic.websocket.jetty9;
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Standard WebSocket tunnel implementation. The classes here require a recent
* servlet container that supports JSR 356.
*/
package org.apache.guacamole.net.basic.websocket;
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.apache.guacamole.net.basic.websocket.tomcat;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.basic.TunnelRequestService;
import org.apache.guacamole.net.basic.TunnelRequest;
/**
* Tunnel servlet implementation which uses WebSocket as a tunnel backend,
* rather than HTTP, properly parsing connection IDs included in the connection
* request.
*/
@Singleton
public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet {
/**
* Service for handling tunnel requests.
*/
@Inject
private TunnelRequestService tunnelRequestService;
@Override
protected GuacamoleTunnel doConnect(TunnelRequest request)
throws GuacamoleException {
return tunnelRequestService.createTunnel(request);
};
}
Loaded 100 of 402 files, more files were not shown because too many files have changed in this diff. Show more