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:
		| @@ -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); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<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; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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<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; | ||||
|  | ||||
|             return createTunnel(request); | ||||
|         } | ||||
|  | ||||
|     }; | ||||
|   | ||||
| @@ -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.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> servlet = (Class<Servlet>) GuacamoleClassLoader.getInstance().findClass( | ||||
|                 "org.glyptodon.guacamole.net.basic.BasicGuacamoleWebSocketTunnelServlet" | ||||
|             ); | ||||
|             Class<Servlet> servlet = (Class<Servlet>) | ||||
|                     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."); | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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