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; +