Add official WebSocket support for both Jetty and Tomcat. Require "enable-websocket" property to be set to "true".

This commit is contained in:
Michael Jumper
2013-10-15 19:04:15 -07:00
13 changed files with 1303 additions and 150 deletions

View File

@@ -134,6 +134,29 @@
<scope>runtime</scope>
</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>
</project>

View File

@@ -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);
}

View File

@@ -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);
}
};

View File

@@ -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;
}
}

View File

@@ -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.");
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

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