mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 00:53:21 +00:00 
			
		
		
		
	Add official WebSocket support for both Jetty and Tomcat. Require "enable-websocket" property to be set to "true".
This commit is contained in:
		| @@ -134,6 +134,29 @@ | |||||||
|             <scope>runtime</scope> |             <scope>runtime</scope> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |         <!-- Jetty servlet API (websocket)  --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.eclipse.jetty</groupId> | ||||||
|  |             <artifactId>jetty-websocket</artifactId> | ||||||
|  |             <version>8.1.1.v20120215</version> | ||||||
|  |             <scope>provided</scope> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <!-- Tomcat servlet API (websocket)  --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.apache.tomcat</groupId> | ||||||
|  |             <artifactId>tomcat-catalina</artifactId> | ||||||
|  |             <version>7.0.37</version> | ||||||
|  |             <scope>provided</scope> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.apache.tomcat</groupId> | ||||||
|  |             <artifactId>tomcat-coyote</artifactId> | ||||||
|  |             <version>7.0.37</version> | ||||||
|  |             <scope>provided</scope> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|     </dependencies> |     </dependencies> | ||||||
|  |  | ||||||
| </project> | </project> | ||||||
|   | |||||||
| @@ -70,12 +70,12 @@ public abstract class AuthenticatingHttpServlet extends HttpServlet { | |||||||
|     /** |     /** | ||||||
|      * The session attribute holding the current UserContext. |      * 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. |      * 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. |      * 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. |      * @param session The session to retrieve credentials from. | ||||||
|      * @return The credentials associated with the given session. |      * @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); |         return (Credentials) session.getAttribute(CREDENTIALS_ATTRIBUTE); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -211,7 +211,7 @@ public abstract class AuthenticatingHttpServlet extends HttpServlet { | |||||||
|      * @param session The session to retrieve UserContext from. |      * @param session The session to retrieve UserContext from. | ||||||
|      * @return The UserContext associated with the given session. |      * @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); |         return (UserContext) session.getAttribute(CONTEXT_ATTRIBUTE); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -56,12 +56,12 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet { | |||||||
|     /** |     /** | ||||||
|      * Logger for this class. |      * Logger for this class. | ||||||
|      */ |      */ | ||||||
|     private Logger logger = LoggerFactory.getLogger(BasicGuacamoleTunnelServlet.class); |     private static Logger logger = LoggerFactory.getLogger(BasicGuacamoleTunnelServlet.class); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * All supported identifier types. |      * All supported identifier types. | ||||||
|      */ |      */ | ||||||
|     private static enum IdentifierType { |     public static enum IdentifierType { | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
|          * The unique identifier of a connection. |          * The unique identifier of a connection. | ||||||
| @@ -156,7 +156,7 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet { | |||||||
|      *                            error, the connect is canceled, and no other |      *                            error, the connect is canceled, and no other | ||||||
|      *                            listeners will run. |      *                            listeners will run. | ||||||
|      */ |      */ | ||||||
|     private boolean notifyConnect(Collection listeners, UserContext context, |     public static boolean notifyConnect(Collection listeners, UserContext context, | ||||||
|             Credentials credentials, GuacamoleTunnel tunnel) |             Credentials credentials, GuacamoleTunnel tunnel) | ||||||
|             throws GuacamoleException { |             throws GuacamoleException { | ||||||
|  |  | ||||||
| @@ -196,7 +196,7 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet { | |||||||
|      *                            error, the close is canceled, and no other |      *                            error, the close is canceled, and no other | ||||||
|      *                            listeners will run. |      *                            listeners will run. | ||||||
|      */ |      */ | ||||||
|     private boolean notifyClose(Collection listeners, UserContext context, |     public static boolean notifyClose(Collection listeners, UserContext context, | ||||||
|             Credentials credentials, GuacamoleTunnel tunnel) |             Credentials credentials, GuacamoleTunnel tunnel) | ||||||
|             throws GuacamoleException { |             throws GuacamoleException { | ||||||
|  |  | ||||||
| @@ -220,13 +220,15 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated |      * Creates a new tunnel using the parameters and credentials present in | ||||||
|      * requests. |      * 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. | ||||||
|      */ |      */ | ||||||
|     private GuacamoleHTTPTunnelServlet tunnelServlet = new GuacamoleHTTPTunnelServlet() { |     public static GuacamoleTunnel createTunnel(HttpServletRequest request) | ||||||
|  |             throws GuacamoleException { | ||||||
|         @Override |  | ||||||
|         protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { |  | ||||||
|  |  | ||||||
|         HttpSession httpSession = request.getSession(true); |         HttpSession httpSession = request.getSession(true); | ||||||
|  |  | ||||||
| @@ -361,6 +363,17 @@ public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet { | |||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated | ||||||
|  |      * requests. | ||||||
|  |      */ | ||||||
|  |     private GuacamoleHTTPTunnelServlet tunnelServlet = new GuacamoleHTTPTunnelServlet() { | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { | ||||||
|  |             return createTunnel(request); | ||||||
|  |         } | ||||||
|  |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<String, Connection> 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<String, ConnectionGroup> 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; | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -25,6 +25,8 @@ import javax.servlet.ServletContext; | |||||||
| import javax.servlet.ServletContextEvent; | import javax.servlet.ServletContextEvent; | ||||||
| import javax.servlet.ServletContextListener; | import javax.servlet.ServletContextListener; | ||||||
| import org.glyptodon.guacamole.GuacamoleException; | import org.glyptodon.guacamole.GuacamoleException; | ||||||
|  | import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty; | ||||||
|  | import org.glyptodon.guacamole.properties.GuacamoleProperties; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
| @@ -37,6 +39,17 @@ import org.slf4j.LoggerFactory; | |||||||
|  * not be available or needed if WebSocket is not desired, the 3.0 API is |  * not be available or needed if WebSocket is not desired, the 3.0 API is | ||||||
|  * detected and invoked dynamically via reflection. |  * 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 |  * @author Michael Jumper | ||||||
|  */ |  */ | ||||||
| public class WebSocketSupportLoader implements ServletContextListener { | public class WebSocketSupportLoader implements ServletContextListener { | ||||||
| @@ -46,19 +59,35 @@ public class WebSocketSupportLoader implements ServletContextListener { | |||||||
|      */ |      */ | ||||||
|     private Logger logger = LoggerFactory.getLogger(WebSocketSupportLoader.class); |     private Logger logger = LoggerFactory.getLogger(WebSocketSupportLoader.class); | ||||||
|  |  | ||||||
|     @Override |     private static final BooleanGuacamoleProperty ENABLE_WEBSOCKET = | ||||||
|     public void contextDestroyed(ServletContextEvent sce) { |             new BooleanGuacamoleProperty() { | ||||||
|     } |  | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|     public void contextInitialized(ServletContextEvent sce) { |         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 { |         try { | ||||||
|  |  | ||||||
|             // Attempt to find WebSocket servlet |             // Attempt to find WebSocket servlet | ||||||
|             Class<Servlet> servlet = (Class<Servlet>) GuacamoleClassLoader.getInstance().findClass( |             Class<Servlet> servlet = (Class<Servlet>) | ||||||
|                 "org.glyptodon.guacamole.net.basic.BasicGuacamoleWebSocketTunnelServlet" |                     GuacamoleClassLoader.getInstance().findClass(classname); | ||||||
|             ); |  | ||||||
|  |  | ||||||
|             // Dynamically add servlet IF SERVLET 3.0 API AVAILABLE! |             // Dynamically add servlet IF SERVLET 3.0 API AVAILABLE! | ||||||
|             try { |             try { | ||||||
| @@ -67,8 +96,9 @@ public class WebSocketSupportLoader implements ServletContextListener { | |||||||
|                 Class regClass = Class.forName("javax.servlet.ServletRegistration"); |                 Class regClass = Class.forName("javax.servlet.ServletRegistration"); | ||||||
|  |  | ||||||
|                 // Get and invoke addServlet() |                 // Get and invoke addServlet() | ||||||
|                 Method addServlet = ServletContext.class.getMethod("addServlet", String.class, Class.class); |                 Method addServlet = ServletContext.class.getMethod("addServlet", | ||||||
|                 Object reg = addServlet.invoke(sce.getServletContext(), "WebSocketTunnel", servlet); |                         String.class, Class.class); | ||||||
|  |                 Object reg = addServlet.invoke(context, "WebSocketTunnel", servlet); | ||||||
|  |  | ||||||
|                 // Get and invoke addMapping() |                 // Get and invoke addMapping() | ||||||
|                 Method addMapping = regClass.getMethod("addMapping", String[].class); |                 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, |                 // If we succesfully load and register the WebSocket tunnel servlet, | ||||||
|                 // WebSocket is supported. |                 // WebSocket is supported. | ||||||
|                 logger.info("WebSocket support found and loaded."); |                 logger.info("WebSocket support found and loaded."); | ||||||
|  |                 return true; | ||||||
|  |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -102,12 +133,53 @@ public class WebSocketSupportLoader implements ServletContextListener { | |||||||
|         catch (ClassNotFoundException e) { |         catch (ClassNotFoundException e) { | ||||||
|             logger.info("WebSocket support not found."); |             logger.info("WebSocket support not found."); | ||||||
|         } |         } | ||||||
|  |         catch (NoClassDefFoundError e) { | ||||||
|  |             logger.info("WebSocket support not found."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // Log all GuacamoleExceptions |         // Log all GuacamoleExceptions | ||||||
|         catch (GuacamoleException e) { |         catch (GuacamoleException e) { | ||||||
|             logger.error("Unable to load/detect WebSocket support.", 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."); | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -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; | ||||||
|  |  | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -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; | ||||||
|  |  | ||||||
		Reference in New Issue
	
	Block a user