diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index f8afcbf3e..df7115cbd 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -134,6 +134,29 @@
runtime
+
+
+ org.eclipse.jetty
+ jetty-websocket
+ 8.1.1.v20120215
+ provided
+
+
+
+
+ org.apache.tomcat
+ tomcat-catalina
+ 7.0.37
+ provided
+
+
+
+ org.apache.tomcat
+ tomcat-coyote
+ 7.0.37
+ provided
+
+
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingHttpServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingHttpServlet.java
index f42d88b19..6b7e96596 100644
--- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingHttpServlet.java
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingHttpServlet.java
@@ -70,12 +70,12 @@ public abstract class AuthenticatingHttpServlet extends HttpServlet {
/**
* The session attribute holding the current UserContext.
*/
- private static final String CONTEXT_ATTRIBUTE = "GUAC_CONTEXT";
+ public static final String CONTEXT_ATTRIBUTE = "GUAC_CONTEXT";
/**
* The session attribute holding the credentials authorizing this session.
*/
- private static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS";
+ public static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS";
/**
* The AuthenticationProvider to use to authenticate all requests.
@@ -201,7 +201,7 @@ public abstract class AuthenticatingHttpServlet extends HttpServlet {
* @param session The session to retrieve credentials from.
* @return The credentials associated with the given session.
*/
- protected Credentials getCredentials(HttpSession session) {
+ public static Credentials getCredentials(HttpSession session) {
return (Credentials) session.getAttribute(CREDENTIALS_ATTRIBUTE);
}
@@ -211,7 +211,7 @@ public abstract class AuthenticatingHttpServlet extends HttpServlet {
* @param session The session to retrieve UserContext from.
* @return The UserContext associated with the given session.
*/
- protected UserContext getUserContext(HttpSession session) {
+ public static UserContext getUserContext(HttpSession session) {
return (UserContext) session.getAttribute(CONTEXT_ATTRIBUTE);
}
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.java
index 015a01cab..86d687816 100644
--- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.java
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.java
@@ -56,12 +56,12 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet {
/**
* Logger for this class.
*/
- private Logger logger = LoggerFactory.getLogger(BasicGuacamoleTunnelServlet.class);
+ private static Logger logger = LoggerFactory.getLogger(BasicGuacamoleTunnelServlet.class);
/**
* All supported identifier types.
*/
- private static enum IdentifierType {
+ public static enum IdentifierType {
/**
* The unique identifier of a connection.
@@ -156,7 +156,7 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet {
* error, the connect is canceled, and no other
* listeners will run.
*/
- private boolean notifyConnect(Collection listeners, UserContext context,
+ public static boolean notifyConnect(Collection listeners, UserContext context,
Credentials credentials, GuacamoleTunnel tunnel)
throws GuacamoleException {
@@ -196,7 +196,7 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet {
* error, the close is canceled, and no other
* listeners will run.
*/
- private boolean notifyClose(Collection listeners, UserContext context,
+ public static boolean notifyClose(Collection listeners, UserContext context,
Credentials credentials, GuacamoleTunnel tunnel)
throws GuacamoleException {
@@ -219,6 +219,150 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet {
}
+ /**
+ * Creates a new tunnel using the parameters and credentials present in
+ * the given request.
+ *
+ * @param request The HttpServletRequest 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 static GuacamoleTunnel createTunnel(HttpServletRequest request)
+ throws GuacamoleException {
+
+ HttpSession httpSession = request.getSession(true);
+
+ // Get listeners
+ final SessionListenerCollection listeners;
+ try {
+ listeners = new SessionListenerCollection(httpSession);
+ }
+ catch (GuacamoleException e) {
+ logger.error("Failed to retrieve listeners. Authentication canceled.", e);
+ throw e;
+ }
+
+ // Get ID of connection
+ String id = request.getParameter("id");
+ IdentifierType id_type = IdentifierType.getType(id);
+
+ // Do not continue if unable to determine type
+ if (id_type == null)
+ throw new GuacamoleClientException("Illegal identifier - unknown type.");
+
+ // Remove prefix
+ id = id.substring(id_type.PREFIX.length());
+
+ // Get credentials
+ final Credentials credentials = getCredentials(httpSession);
+
+ // Get context
+ final UserContext context = getUserContext(httpSession);
+
+ // If no context or no credentials, not logged in
+ if (context == null || credentials == null)
+ throw new GuacamoleSecurityException("Cannot connect - user not logged in.");
+
+ // Get client information
+ GuacamoleClientInformation info = new GuacamoleClientInformation();
+
+ // Set width if provided
+ String width = request.getParameter("width");
+ if (width != null)
+ info.setOptimalScreenWidth(Integer.parseInt(width));
+
+ // Set height if provided
+ String height = request.getParameter("height");
+ if (height != null)
+ info.setOptimalScreenHeight(Integer.parseInt(height));
+
+ // Add audio mimetypes
+ String[] audio_mimetypes = request.getParameterValues("audio");
+ if (audio_mimetypes != null)
+ info.getAudioMimetypes().addAll(Arrays.asList(audio_mimetypes));
+
+ // Add video mimetypes
+ String[] video_mimetypes = request.getParameterValues("video");
+ if (video_mimetypes != null)
+ info.getVideoMimetypes().addAll(Arrays.asList(video_mimetypes));
+
+ // Create connected socket from identifier
+ GuacamoleSocket socket;
+ switch (id_type) {
+
+ // Connection identifiers
+ case CONNECTION: {
+
+ // Get connection directory
+ Directory directory =
+ context.getRootConnectionGroup().getConnectionDirectory();
+
+ // Get authorized connection
+ Connection connection = directory.get(id);
+ if (connection == null) {
+ logger.warn("Connection id={} not found.", id);
+ throw new GuacamoleSecurityException("Requested connection is not authorized.");
+ }
+
+ // Connect socket
+ socket = connection.connect(info);
+ logger.info("Successful connection from {} to \"{}\".", request.getRemoteAddr(), id);
+ break;
+ }
+
+ // Connection group identifiers
+ case CONNECTION_GROUP: {
+
+ // Get connection group directory
+ Directory directory =
+ context.getRootConnectionGroup().getConnectionGroupDirectory();
+
+ // Get authorized connection group
+ ConnectionGroup group = directory.get(id);
+ if (group == null) {
+ logger.warn("Connection group id={} not found.", id);
+ throw new GuacamoleSecurityException("Requested connection group is not authorized.");
+ }
+
+ // Connect socket
+ socket = group.connect(info);
+ logger.info("Successful connection from {} to group \"{}\".", request.getRemoteAddr(), id);
+ break;
+ }
+
+ // Fail if unsupported type
+ default:
+ throw new GuacamoleClientException("Connection not supported for provided identifier type.");
+
+ }
+
+ // Associate socket with tunnel
+ GuacamoleTunnel tunnel = new GuacamoleTunnel(socket) {
+
+ @Override
+ public void close() throws GuacamoleException {
+
+ // Only close if not canceled
+ if (!notifyClose(listeners, context, credentials, this))
+ throw new GuacamoleException("Tunnel close canceled by listener.");
+
+ // Close if no exception due to listener
+ super.close();
+
+ }
+
+ };
+
+ // Notify listeners about connection
+ if (!notifyConnect(listeners, context, credentials, tunnel)) {
+ logger.info("Connection canceled by listener.");
+ return null;
+ }
+
+ return tunnel;
+
+ }
+
/**
* Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
* requests.
@@ -227,138 +371,7 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet {
@Override
protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException {
-
- HttpSession httpSession = request.getSession(true);
-
- // Get listeners
- final SessionListenerCollection listeners;
- try {
- listeners = new SessionListenerCollection(httpSession);
- }
- catch (GuacamoleException e) {
- logger.error("Failed to retrieve listeners. Authentication canceled.", e);
- throw e;
- }
-
- // Get ID of connection
- String id = request.getParameter("id");
- IdentifierType id_type = IdentifierType.getType(id);
-
- // Do not continue if unable to determine type
- if (id_type == null)
- throw new GuacamoleClientException("Illegal identifier - unknown type.");
-
- // Remove prefix
- id = id.substring(id_type.PREFIX.length());
-
- // Get credentials
- final Credentials credentials = getCredentials(httpSession);
-
- // Get context
- final UserContext context = getUserContext(httpSession);
-
- // If no context or no credentials, not logged in
- if (context == null || credentials == null)
- throw new GuacamoleSecurityException("Cannot connect - user not logged in.");
-
- // Get client information
- GuacamoleClientInformation info = new GuacamoleClientInformation();
-
- // Set width if provided
- String width = request.getParameter("width");
- if (width != null)
- info.setOptimalScreenWidth(Integer.parseInt(width));
-
- // Set height if provided
- String height = request.getParameter("height");
- if (height != null)
- info.setOptimalScreenHeight(Integer.parseInt(height));
-
- // Add audio mimetypes
- String[] audio_mimetypes = request.getParameterValues("audio");
- if (audio_mimetypes != null)
- info.getAudioMimetypes().addAll(Arrays.asList(audio_mimetypes));
-
- // Add video mimetypes
- String[] video_mimetypes = request.getParameterValues("video");
- if (video_mimetypes != null)
- info.getVideoMimetypes().addAll(Arrays.asList(video_mimetypes));
-
- // Create connected socket from identifier
- GuacamoleSocket socket;
- switch (id_type) {
-
- // Connection identifiers
- case CONNECTION: {
-
- // Get connection directory
- Directory directory =
- context.getRootConnectionGroup().getConnectionDirectory();
-
- // Get authorized connection
- Connection connection = directory.get(id);
- if (connection == null) {
- logger.warn("Connection id={} not found.", id);
- throw new GuacamoleSecurityException("Requested connection is not authorized.");
- }
-
- // Connect socket
- socket = connection.connect(info);
- logger.info("Successful connection from {} to \"{}\".", request.getRemoteAddr(), id);
- break;
- }
-
- // Connection group identifiers
- case CONNECTION_GROUP: {
-
- // Get connection group directory
- Directory directory =
- context.getRootConnectionGroup().getConnectionGroupDirectory();
-
- // Get authorized connection group
- ConnectionGroup group = directory.get(id);
- if (group == null) {
- logger.warn("Connection group id={} not found.", id);
- throw new GuacamoleSecurityException("Requested connection group is not authorized.");
- }
-
- // Connect socket
- socket = group.connect(info);
- logger.info("Successful connection from {} to group \"{}\".", request.getRemoteAddr(), id);
- break;
- }
-
- // Fail if unsupported type
- default:
- throw new GuacamoleClientException("Connection not supported for provided identifier type.");
-
- }
-
- // Associate socket with tunnel
- GuacamoleTunnel tunnel = new GuacamoleTunnel(socket) {
-
- @Override
- public void close() throws GuacamoleException {
-
- // Only close if not canceled
- if (!notifyClose(listeners, context, credentials, this))
- throw new GuacamoleException("Tunnel close canceled by listener.");
-
- // Close if no exception due to listener
- super.close();
-
- }
-
- };
-
- // Notify listeners about connection
- if (!notifyConnect(listeners, context, credentials, tunnel)) {
- logger.info("Connection canceled by listener.");
- return null;
- }
-
- return tunnel;
-
+ return createTunnel(request);
}
};
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java
new file mode 100644
index 000000000..57f8184ee
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java
@@ -0,0 +1,342 @@
+package org.glyptodon.guacamole.net.basic;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import org.glyptodon.guacamole.GuacamoleClientException;
+import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.GuacamoleSecurityException;
+import org.glyptodon.guacamole.net.GuacamoleSocket;
+import org.glyptodon.guacamole.net.GuacamoleTunnel;
+import org.glyptodon.guacamole.net.auth.Connection;
+import org.glyptodon.guacamole.net.auth.ConnectionGroup;
+import org.glyptodon.guacamole.net.auth.Credentials;
+import org.glyptodon.guacamole.net.auth.Directory;
+import org.glyptodon.guacamole.net.auth.UserContext;
+import org.glyptodon.guacamole.net.basic.event.SessionListenerCollection;
+import org.glyptodon.guacamole.net.event.TunnelCloseEvent;
+import org.glyptodon.guacamole.net.event.TunnelConnectEvent;
+import org.glyptodon.guacamole.net.event.listener.TunnelCloseListener;
+import org.glyptodon.guacamole.net.event.listener.TunnelConnectListener;
+import org.glyptodon.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
+ */
+public class BasicTunnelRequestUtility {
+
+ /**
+ * Logger for this class.
+ */
+ private static Logger logger = LoggerFactory.getLogger(BasicTunnelRequestUtility.class);
+
+ /**
+ * All supported identifier types.
+ */
+ private static enum IdentifierType {
+
+ /**
+ * The unique identifier of a connection.
+ */
+ CONNECTION("c/"),
+
+ /**
+ * The unique identifier of a connection group.
+ */
+ CONNECTION_GROUP("g/");
+
+ /**
+ * The prefix which precedes an identifier of this type.
+ */
+ final String PREFIX;
+
+ /**
+ * Defines an IdentifierType having the given prefix.
+ * @param prefix The prefix which will precede any identifier of this
+ * type, thus differentiating it from other identifier
+ * types.
+ */
+ IdentifierType(String prefix) {
+ PREFIX = prefix;
+ }
+
+ /**
+ * Given an identifier, determines the corresponding identifier type.
+ *
+ * @param identifier The identifier whose type should be identified.
+ * @return The identified identifier type.
+ */
+ static IdentifierType getType(String identifier) {
+
+ // If null, no known identifier
+ if (identifier == null)
+ return null;
+
+ // Connection identifiers
+ if (identifier.startsWith(CONNECTION.PREFIX))
+ return CONNECTION;
+
+ // Connection group identifiers
+ if (identifier.startsWith(CONNECTION_GROUP.PREFIX))
+ return CONNECTION_GROUP;
+
+ // Otherwise, unknown
+ return null;
+
+ }
+
+ };
+
+ /**
+ * Notifies all listeners in the given collection that a tunnel has been
+ * connected.
+ *
+ * @param listeners A collection of all listeners that should be notified.
+ * @param context The UserContext associated with the current session.
+ * @param credentials The credentials associated with the current session.
+ * @param tunnel The tunnel being connected.
+ * @return true if all listeners are allowing the tunnel to connect,
+ * or if there are no listeners, and false if any listener is
+ * canceling the connection. Note that once one listener cancels,
+ * no other listeners will run.
+ * @throws GuacamoleException If any listener throws an error while being
+ * notified. Note that if any listener throws an
+ * error, the connect is canceled, and no other
+ * listeners will run.
+ */
+ private static boolean notifyConnect(Collection listeners, UserContext context,
+ Credentials credentials, GuacamoleTunnel tunnel)
+ throws GuacamoleException {
+
+ // Build event for auth success
+ TunnelConnectEvent event = new TunnelConnectEvent(context,
+ credentials, tunnel);
+
+ // Notify all listeners
+ for (Object listener : listeners) {
+ if (listener instanceof TunnelConnectListener) {
+
+ // Cancel immediately if hook returns false
+ if (!((TunnelConnectListener) listener).tunnelConnected(event))
+ return false;
+
+ }
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Notifies all listeners in the given collection that a tunnel has been
+ * closed.
+ *
+ * @param listeners A collection of all listeners that should be notified.
+ * @param context The UserContext associated with the current session.
+ * @param credentials The credentials associated with the current session.
+ * @param tunnel The tunnel being closed.
+ * @return true if all listeners are allowing the tunnel to close,
+ * or if there are no listeners, and false if any listener is
+ * canceling the close. Note that once one listener cancels,
+ * no other listeners will run.
+ * @throws GuacamoleException If any listener throws an error while being
+ * notified. Note that if any listener throws an
+ * error, the close is canceled, and no other
+ * listeners will run.
+ */
+ private static boolean notifyClose(Collection listeners, UserContext context,
+ Credentials credentials, GuacamoleTunnel tunnel)
+ throws GuacamoleException {
+
+ // Build event for auth success
+ TunnelCloseEvent event = new TunnelCloseEvent(context,
+ credentials, tunnel);
+
+ // Notify all listeners
+ for (Object listener : listeners) {
+ if (listener instanceof TunnelCloseListener) {
+
+ // Cancel immediately if hook returns false
+ if (!((TunnelCloseListener) listener).tunnelClosed(event))
+ return false;
+
+ }
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Creates a new tunnel using the parameters and credentials present in
+ * the given request.
+ *
+ * @param request The HttpServletRequest 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 static GuacamoleTunnel createTunnel(HttpServletRequest request)
+ throws GuacamoleException {
+
+ HttpSession httpSession = request.getSession(true);
+
+ // Get listeners
+ final SessionListenerCollection listeners;
+ try {
+ listeners = new SessionListenerCollection(httpSession);
+ }
+ catch (GuacamoleException e) {
+ logger.error("Failed to retrieve listeners. Authentication canceled.", e);
+ throw e;
+ }
+
+ // Get ID of connection
+ String id = request.getParameter("id");
+ IdentifierType id_type = IdentifierType.getType(id);
+
+ // Do not continue if unable to determine type
+ if (id_type == null)
+ throw new GuacamoleClientException("Illegal identifier - unknown type.");
+
+ // Remove prefix
+ id = id.substring(id_type.PREFIX.length());
+
+ // Get credentials
+ final Credentials credentials = AuthenticatingHttpServlet.getCredentials(httpSession);
+
+ // Get context
+ final UserContext context = AuthenticatingHttpServlet.getUserContext(httpSession);
+
+ // If no context or no credentials, not logged in
+ if (context == null || credentials == null)
+ throw new GuacamoleSecurityException("Cannot connect - user not logged in.");
+
+ // Get client information
+ GuacamoleClientInformation info = new GuacamoleClientInformation();
+
+ // Set width if provided
+ String width = request.getParameter("width");
+ if (width != null)
+ info.setOptimalScreenWidth(Integer.parseInt(width));
+
+ // Set height if provided
+ String height = request.getParameter("height");
+ if (height != null)
+ info.setOptimalScreenHeight(Integer.parseInt(height));
+
+ // Add audio mimetypes
+ String[] audio_mimetypes = request.getParameterValues("audio");
+ if (audio_mimetypes != null)
+ info.getAudioMimetypes().addAll(Arrays.asList(audio_mimetypes));
+
+ // Add video mimetypes
+ String[] video_mimetypes = request.getParameterValues("video");
+ if (video_mimetypes != null)
+ info.getVideoMimetypes().addAll(Arrays.asList(video_mimetypes));
+
+ // Create connected socket from identifier
+ GuacamoleSocket socket;
+ switch (id_type) {
+
+ // Connection identifiers
+ case CONNECTION: {
+
+ // Get connection directory
+ Directory directory =
+ context.getRootConnectionGroup().getConnectionDirectory();
+
+ // Get authorized connection
+ Connection connection = directory.get(id);
+ if (connection == null) {
+ logger.warn("Connection id={} not found.", id);
+ throw new GuacamoleSecurityException("Requested connection is not authorized.");
+ }
+
+ // Connect socket
+ socket = connection.connect(info);
+ logger.info("Successful connection from {} to \"{}\".", request.getRemoteAddr(), id);
+ break;
+ }
+
+ // Connection group identifiers
+ case CONNECTION_GROUP: {
+
+ // Get connection group directory
+ Directory directory =
+ context.getRootConnectionGroup().getConnectionGroupDirectory();
+
+ // Get authorized connection group
+ ConnectionGroup group = directory.get(id);
+ if (group == null) {
+ logger.warn("Connection group id={} not found.", id);
+ throw new GuacamoleSecurityException("Requested connection group is not authorized.");
+ }
+
+ // Connect socket
+ socket = group.connect(info);
+ logger.info("Successful connection from {} to group \"{}\".", request.getRemoteAddr(), id);
+ break;
+ }
+
+ // Fail if unsupported type
+ default:
+ throw new GuacamoleClientException("Connection not supported for provided identifier type.");
+
+ }
+
+ // Associate socket with tunnel
+ GuacamoleTunnel tunnel = new GuacamoleTunnel(socket) {
+
+ @Override
+ public void close() throws GuacamoleException {
+
+ // Only close if not canceled
+ if (!notifyClose(listeners, context, credentials, this))
+ throw new GuacamoleException("Tunnel close canceled by listener.");
+
+ // Close if no exception due to listener
+ super.close();
+
+ }
+
+ };
+
+ // Notify listeners about connection
+ if (!notifyConnect(listeners, context, credentials, tunnel)) {
+ logger.info("Connection canceled by listener.");
+ return null;
+ }
+
+ return tunnel;
+
+ }
+
+}
+
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/WebSocketSupportLoader.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/WebSocketSupportLoader.java
index ecf88c24b..12b0fc2b6 100644
--- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/WebSocketSupportLoader.java
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/WebSocketSupportLoader.java
@@ -25,6 +25,8 @@ import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty;
+import org.glyptodon.guacamole.properties.GuacamoleProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,6 +38,17 @@ import org.slf4j.LoggerFactory;
* Note that because Guacamole depends on the Servlet 2.5 API, and 3.0 may
* not be available or needed if WebSocket is not desired, the 3.0 API is
* detected and invoked dynamically via reflection.
+ *
+ * Tests have shown that while WebSocket is negligibly more responsive than
+ * Guacamole's native HTTP tunnel, downstream performance is not yet a match.
+ * This may be because browser WebSocket implementations are not optimized for
+ * throughput, or it may be because servlet container WebSocket implementations
+ * are in their infancy, or it may be that OUR WebSocket-backed tunnel
+ * implementations are not efficient. Because of this, WebSocket support is
+ * disabled by default. To enable it, add the following property to
+ * your guacamole.properties:
+ *
+ * enable-websocket: true
*
* @author Michael Jumper
*/
@@ -46,19 +59,35 @@ public class WebSocketSupportLoader implements ServletContextListener {
*/
private Logger logger = LoggerFactory.getLogger(WebSocketSupportLoader.class);
- @Override
- public void contextDestroyed(ServletContextEvent sce) {
- }
+ private static final BooleanGuacamoleProperty ENABLE_WEBSOCKET =
+ new BooleanGuacamoleProperty() {
- @Override
- public void contextInitialized(ServletContextEvent sce) {
+ @Override
+ public String getName() {
+ return "enable-websocket";
+ }
+
+ };
+
+ /**
+ * Classname of the Jetty-specific WebSocket tunnel implementation.
+ */
+ private static final String JETTY_WEBSOCKET =
+ "org.glyptodon.guacamole.net.basic.websocket.jetty.BasicGuacamoleWebSocketTunnelServlet";
+
+ /**
+ * Classname of the Tomcat-specific WebSocket tunnel implementation.
+ */
+ private static final String TOMCAT_WEBSOCKET =
+ "org.glyptodon.guacamole.net.basic.websocket.tomcat.BasicGuacamoleWebSocketTunnelServlet";
+
+ private boolean loadWebSocketTunnel(ServletContext context, String classname) {
try {
// Attempt to find WebSocket servlet
- Class servlet = (Class) GuacamoleClassLoader.getInstance().findClass(
- "org.glyptodon.guacamole.net.basic.BasicGuacamoleWebSocketTunnelServlet"
- );
+ Class servlet = (Class)
+ GuacamoleClassLoader.getInstance().findClass(classname);
// Dynamically add servlet IF SERVLET 3.0 API AVAILABLE!
try {
@@ -67,8 +96,9 @@ public class WebSocketSupportLoader implements ServletContextListener {
Class regClass = Class.forName("javax.servlet.ServletRegistration");
// Get and invoke addServlet()
- Method addServlet = ServletContext.class.getMethod("addServlet", String.class, Class.class);
- Object reg = addServlet.invoke(sce.getServletContext(), "WebSocketTunnel", servlet);
+ Method addServlet = ServletContext.class.getMethod("addServlet",
+ String.class, Class.class);
+ Object reg = addServlet.invoke(context, "WebSocketTunnel", servlet);
// Get and invoke addMapping()
Method addMapping = regClass.getMethod("addMapping", String[].class);
@@ -77,6 +107,7 @@ public class WebSocketSupportLoader implements ServletContextListener {
// If we succesfully load and register the WebSocket tunnel servlet,
// WebSocket is supported.
logger.info("WebSocket support found and loaded.");
+ return true;
}
@@ -102,12 +133,53 @@ public class WebSocketSupportLoader implements ServletContextListener {
catch (ClassNotFoundException e) {
logger.info("WebSocket support not found.");
}
+ catch (NoClassDefFoundError e) {
+ logger.info("WebSocket support not found.");
+ }
// Log all GuacamoleExceptions
catch (GuacamoleException e) {
logger.error("Unable to load/detect WebSocket support.", e);
}
+ // Load attempt failed
+ return false;
+
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce) {
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+
+ try {
+
+ // Stop if WebSocket not explicitly enabled.
+ if (!GuacamoleProperties.getProperty(ENABLE_WEBSOCKET, false)) {
+ logger.info("WebSocket support not enabled.");
+ return;
+ }
+
+ }
+ catch (GuacamoleException e) {
+ logger.error("Error parsing enable-websocket property.", e);
+ }
+
+ // Try to load websocket support for Jetty
+ logger.info("Attempting to load Jetty-specific WebSocket support...");
+ if (loadWebSocketTunnel(sce.getServletContext(), JETTY_WEBSOCKET))
+ return;
+
+ // Try to load websocket support for Tomcat
+ logger.info("Attempting to load Tomcat-specific WebSocket support...");
+ if (loadWebSocketTunnel(sce.getServletContext(), TOMCAT_WEBSOCKET))
+ return;
+
+ // Inform of lack of support
+ logger.info("No WebSocket support could be loaded. Only HTTP will be used.");
+
}
}
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/AuthenticatingWebSocketServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/AuthenticatingWebSocketServlet.java
new file mode 100644
index 000000000..d36b14212
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/AuthenticatingWebSocketServlet.java
@@ -0,0 +1,137 @@
+
+package org.glyptodon.guacamole.net.basic.websocket.jetty;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.GuacamoleServerException;
+import org.glyptodon.guacamole.net.auth.UserContext;
+import org.glyptodon.guacamole.net.basic.AuthenticatingHttpServlet;
+import org.eclipse.jetty.websocket.WebSocket;
+import org.eclipse.jetty.websocket.WebSocketServlet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A WebSocket servlet wrapped around an AuthenticatingHttpServlet.
+ *
+ * @author Michael Jumper
+ */
+public abstract class AuthenticatingWebSocketServlet extends WebSocketServlet {
+
+ /**
+ * Logger for this class.
+ */
+ private Logger logger = LoggerFactory.getLogger(AuthenticatingWebSocketServlet.class);
+
+ /**
+ * Wrapped authenticating servlet.
+ */
+ private AuthenticatingHttpServlet auth_servlet =
+ new AuthenticatingHttpServlet() {
+
+ @Override
+ protected void authenticatedService(UserContext context,
+ HttpServletRequest request, HttpServletResponse response)
+ throws GuacamoleException {
+
+ try {
+ // If authenticated, service request
+ service_websocket_request(request, response);
+ }
+ catch (IOException e) {
+ throw new GuacamoleServerException(
+ "Cannot service WebSocket request (I/O error).", e);
+ }
+ catch (ServletException e) {
+ throw new GuacamoleServerException(
+ "Cannot service WebSocket request (internal error).", e);
+ }
+
+ }
+
+ };
+
+ @Override
+ protected void service(HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+
+ // Authenticate all inbound requests
+ auth_servlet.service(request, response);
+
+ }
+
+ /**
+ * Actually services the given request, bypassing the service() override
+ * and the authentication scheme.
+ *
+ * @param request The HttpServletRequest to service.
+ * @param response The associated HttpServletResponse.
+ * @throws IOException If an I/O error occurs while handling the request.
+ * @throws ServletException If an internal error occurs while handling the
+ * request.
+ */
+ private void service_websocket_request(HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+
+ // Bypass override and service WebSocket request
+ super.service(request, response);
+
+ }
+
+ @Override
+ public WebSocket doWebSocketConnect(HttpServletRequest request,
+ String protocol) {
+
+ // Get session and user context
+ HttpSession session = request.getSession(true);
+ UserContext context = AuthenticatingHttpServlet.getUserContext(session);
+
+ // Ensure user logged in
+ if (context == null) {
+ logger.warn("User no longer logged in upon WebSocket connect.");
+ return null;
+ }
+
+ // Connect WebSocket
+ return authenticatedConnect(context, request, protocol);
+
+ }
+
+ /**
+ * Function called after the credentials given in the request (if any)
+ * are authenticated. If the current session is not associated with
+ * valid credentials, this function will not be called.
+ *
+ * @param context The current UserContext.
+ * @param request The HttpServletRequest being serviced.
+ * @param protocol The protocol being used over the WebSocket connection.
+ */
+ protected abstract WebSocket authenticatedConnect(
+ UserContext context,
+ HttpServletRequest request, String protocol);
+
+}
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/BasicGuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/BasicGuacamoleWebSocketTunnelServlet.java
new file mode 100644
index 000000000..7fec8bdef
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/BasicGuacamoleWebSocketTunnelServlet.java
@@ -0,0 +1,57 @@
+package org.glyptodon.guacamole.net.basic.websocket.jetty;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import javax.servlet.http.HttpServletRequest;
+import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.net.GuacamoleTunnel;
+import org.glyptodon.guacamole.net.auth.UserContext;
+import org.eclipse.jetty.websocket.WebSocket;
+import org.glyptodon.guacamole.net.basic.BasicGuacamoleTunnelServlet;
+
+/**
+ * Authenticating tunnel servlet implementation which uses WebSocket as a
+ * tunnel backend, rather than HTTP.
+ */
+public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet {
+
+ /**
+ * Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
+ * requests.
+ */
+ private GuacamoleWebSocketTunnelServlet tunnelServlet =
+ new GuacamoleWebSocketTunnelServlet() {
+
+ @Override
+ protected GuacamoleTunnel doConnect(HttpServletRequest request)
+ throws GuacamoleException {
+ return BasicGuacamoleTunnelServlet.createTunnel(request);
+ }
+
+ };
+
+
+ @Override
+ protected WebSocket authenticatedConnect(UserContext context,
+ HttpServletRequest request, String protocol) {
+ return tunnelServlet.doWebSocketConnect(request, protocol);
+ }
+
+}
+
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/GuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/GuacamoleWebSocketTunnelServlet.java
new file mode 100644
index 000000000..e4deb5ccb
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/GuacamoleWebSocketTunnelServlet.java
@@ -0,0 +1,133 @@
+package org.glyptodon.guacamole.net.basic.websocket.jetty;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.io.GuacamoleReader;
+import org.glyptodon.guacamole.io.GuacamoleWriter;
+import org.glyptodon.guacamole.net.GuacamoleTunnel;
+import org.eclipse.jetty.websocket.WebSocket;
+import org.eclipse.jetty.websocket.WebSocket.Connection;
+import org.eclipse.jetty.websocket.WebSocketServlet;
+
+/**
+ * A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet.
+ *
+ * @author Michael Jumper
+ */
+public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
+
+ /**
+ * The default, minimum buffer size for instructions.
+ */
+ private static final int BUFFER_SIZE = 8192;
+
+ @Override
+ public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
+
+ // Get tunnel
+ final GuacamoleTunnel tunnel;
+
+ try {
+ tunnel = doConnect(request);
+ }
+ catch (GuacamoleException e) {
+ return null; // FIXME: Can throw exception?
+ }
+
+ // Return new WebSocket which communicates through tunnel
+ return new WebSocket.OnTextMessage() {
+
+ @Override
+ public void onMessage(String string) {
+ GuacamoleWriter writer = tunnel.acquireWriter();
+
+ // Write message received
+ try {
+ writer.write(string.toCharArray());
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+
+ tunnel.releaseWriter();
+ }
+
+ @Override
+ public void onOpen(final Connection connection) {
+
+ Thread readThread = new Thread() {
+
+ @Override
+ public void run() {
+
+ StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
+ GuacamoleReader reader = tunnel.acquireReader();
+ char[] readMessage;
+
+ try {
+ 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);
+ }
+
+ }
+ }
+ catch (IOException e) {
+ // FIXME: Handle exception
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+
+ }
+
+ };
+
+ readThread.start();
+
+ }
+
+ @Override
+ public void onClose(int i, String string) {
+ try {
+ tunnel.close();
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+ }
+
+ };
+
+ }
+
+ protected abstract GuacamoleTunnel doConnect(HttpServletRequest request)
+ throws GuacamoleException;
+
+}
+
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/package-info.java
new file mode 100644
index 000000000..076a983e9
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty/package-info.java
@@ -0,0 +1,8 @@
+
+/**
+ * Jetty WebSocket tunnel implementation. The classes here require at least
+ * Jetty 8, and may change significantly as there is no common WebSocket
+ * API for Java yet.
+ */
+package org.glyptodon.guacamole.net.basic.websocket.jetty;
+
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/AuthenticatingWebSocketServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/AuthenticatingWebSocketServlet.java
new file mode 100644
index 000000000..97c1dd022
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/AuthenticatingWebSocketServlet.java
@@ -0,0 +1,137 @@
+
+package org.glyptodon.guacamole.net.basic.websocket.tomcat;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.GuacamoleServerException;
+import org.glyptodon.guacamole.net.auth.UserContext;
+import org.glyptodon.guacamole.net.basic.AuthenticatingHttpServlet;
+import org.apache.catalina.websocket.StreamInbound;
+import org.apache.catalina.websocket.WebSocketServlet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A WebSocket servlet wrapped around an AuthenticatingHttpServlet.
+ *
+ * @author Michael Jumper
+ */
+public abstract class AuthenticatingWebSocketServlet extends WebSocketServlet {
+
+ /**
+ * Logger for this class.
+ */
+ private Logger logger = LoggerFactory.getLogger(AuthenticatingWebSocketServlet.class);
+
+ /**
+ * Wrapped authenticating servlet.
+ */
+ private AuthenticatingHttpServlet auth_servlet =
+ new AuthenticatingHttpServlet() {
+
+ @Override
+ protected void authenticatedService(UserContext context,
+ HttpServletRequest request, HttpServletResponse response)
+ throws GuacamoleException {
+
+ try {
+ // If authenticated, service request
+ service_websocket_request(request, response);
+ }
+ catch (IOException e) {
+ throw new GuacamoleServerException(
+ "Cannot service WebSocket request (I/O error).", e);
+ }
+ catch (ServletException e) {
+ throw new GuacamoleServerException(
+ "Cannot service WebSocket request (internal error).", e);
+ }
+
+ }
+
+ };
+
+ @Override
+ protected void service(HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+
+ // Authenticate all inbound requests
+ auth_servlet.service(request, response);
+
+ }
+
+ /**
+ * Actually services the given request, bypassing the service() override
+ * and the authentication scheme.
+ *
+ * @param request The HttpServletRequest to service.
+ * @param response The associated HttpServletResponse.
+ * @throws IOException If an I/O error occurs while handling the request.
+ * @throws ServletException If an internal error occurs while handling the
+ * request.
+ */
+ private void service_websocket_request(HttpServletRequest request,
+ HttpServletResponse response)
+ throws IOException, ServletException {
+
+ // Bypass override and service WebSocket request
+ super.service(request, response);
+
+ }
+
+ @Override
+ public StreamInbound createWebSocketInbound(String protocol,
+ HttpServletRequest request) {
+
+ // Get session and user context
+ HttpSession session = request.getSession(true);
+ UserContext context = AuthenticatingHttpServlet.getUserContext(session);
+
+ // Ensure user logged in
+ if (context == null) {
+ logger.warn("User no longer logged in upon WebSocket connect.");
+ return null;
+ }
+
+ // Connect WebSocket
+ return authenticatedConnect(context, request, protocol);
+
+ }
+
+ /**
+ * Function called after the credentials given in the request (if any)
+ * are authenticated. If the current session is not associated with
+ * valid credentials, this function will not be called.
+ *
+ * @param context The current UserContext.
+ * @param request The HttpServletRequest being serviced.
+ * @param protocol The protocol being used over the WebSocket connection.
+ */
+ protected abstract StreamInbound authenticatedConnect(
+ UserContext context,
+ HttpServletRequest request, String protocol);
+
+}
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java
new file mode 100644
index 000000000..e1234c0f5
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java
@@ -0,0 +1,57 @@
+package org.glyptodon.guacamole.net.basic.websocket.tomcat;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import javax.servlet.http.HttpServletRequest;
+import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.net.GuacamoleTunnel;
+import org.glyptodon.guacamole.net.auth.UserContext;
+import org.apache.catalina.websocket.StreamInbound;
+import org.glyptodon.guacamole.net.basic.BasicGuacamoleTunnelServlet;
+
+/**
+ * Authenticating tunnel servlet implementation which uses WebSocket as a
+ * tunnel backend, rather than HTTP.
+ */
+public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet {
+
+ /**
+ * Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
+ * requests.
+ */
+ private GuacamoleWebSocketTunnelServlet tunnelServlet =
+ new GuacamoleWebSocketTunnelServlet() {
+
+ @Override
+ protected GuacamoleTunnel doConnect(HttpServletRequest request)
+ throws GuacamoleException {
+ return BasicGuacamoleTunnelServlet.createTunnel(request);
+ }
+
+ };
+
+
+ @Override
+ protected StreamInbound authenticatedConnect(UserContext context,
+ HttpServletRequest request, String protocol) {
+ return tunnelServlet.createWebSocketInbound(protocol, request);
+ }
+
+}
+
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java
new file mode 100644
index 000000000..8fca84e6e
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java
@@ -0,0 +1,166 @@
+package org.glyptodon.guacamole.net.basic.websocket.tomcat;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.CharBuffer;
+import javax.servlet.http.HttpServletRequest;
+import org.glyptodon.guacamole.GuacamoleException;
+import org.glyptodon.guacamole.io.GuacamoleReader;
+import org.glyptodon.guacamole.io.GuacamoleWriter;
+import org.glyptodon.guacamole.net.GuacamoleTunnel;
+import org.apache.catalina.websocket.StreamInbound;
+import org.apache.catalina.websocket.WebSocketServlet;
+import org.apache.catalina.websocket.WsOutbound;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet.
+ *
+ * @author Michael Jumper
+ */
+public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
+
+ /**
+ * The default, minimum buffer size for instructions.
+ */
+ private static final int BUFFER_SIZE = 8192;
+
+ /**
+ * Logger for this class.
+ */
+ private Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelServlet.class);
+
+ @Override
+ public StreamInbound createWebSocketInbound(String protocol, HttpServletRequest request) {
+
+ // Get tunnel
+ final GuacamoleTunnel tunnel;
+
+ try {
+ tunnel = doConnect(request);
+ }
+ catch (GuacamoleException e) {
+ logger.error("Error connecting WebSocket tunnel.", e);
+ return null;
+ }
+
+ // Return new WebSocket which communicates through tunnel
+ return new StreamInbound() {
+
+ @Override
+ protected void onTextData(Reader reader) throws IOException {
+
+ GuacamoleWriter writer = tunnel.acquireWriter();
+
+ // Write all available data
+ try {
+
+ char[] buffer = new char[BUFFER_SIZE];
+
+ int num_read;
+ while ((num_read = reader.read(buffer)) > 0)
+ writer.write(buffer, 0, num_read);
+
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+
+ tunnel.releaseWriter();
+ }
+
+ @Override
+ public void onOpen(final WsOutbound outbound) {
+
+ Thread readThread = new Thread() {
+
+ @Override
+ public void run() {
+
+ CharBuffer charBuffer = CharBuffer.allocate(BUFFER_SIZE);
+ StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
+ GuacamoleReader reader = tunnel.acquireReader();
+ char[] readMessage;
+
+ try {
+ 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) {
+
+ // Reallocate buffer if necessary
+ if (buffer.length() > charBuffer.length())
+ charBuffer = CharBuffer.allocate(buffer.length());
+ else
+ charBuffer.clear();
+
+ charBuffer.put(buffer.toString().toCharArray());
+ charBuffer.flip();
+
+ outbound.writeTextMessage(charBuffer);
+ buffer.setLength(0);
+ }
+
+ }
+ }
+ catch (IOException e) {
+ // FIXME: Handle exception
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+
+ }
+
+ };
+
+ readThread.start();
+
+ }
+
+ @Override
+ public void onClose(int i) {
+ try {
+ tunnel.close();
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+ }
+
+ @Override
+ protected void onBinaryData(InputStream in) throws IOException {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ };
+
+ }
+
+ protected abstract GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException;
+
+}
+
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/package-info.java
new file mode 100644
index 000000000..61f1b37f1
--- /dev/null
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/package-info.java
@@ -0,0 +1,8 @@
+
+/**
+ * Tomcat WebSocket tunnel implementation. The classes here require at least
+ * Tomcat 7.0, and may change significantly as there is no common WebSocket
+ * API for Java yet.
+ */
+package org.glyptodon.guacamole.net.basic.websocket.tomcat;
+