diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index f8afcbf3e..df7115cbd 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -134,6 +134,29 @@
runtime
+
+
+ org.eclipse.jetty
+ jetty-websocket
+ 8.1.1.v20120215
+ provided
+
+
+
+
+ org.apache.tomcat
+ tomcat-catalina
+ 7.0.37
+ provided
+
+
+
+ org.apache.tomcat
+ tomcat-coyote
+ 7.0.37
+ provided
+
+
diff --git a/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/AuthenticatingWebSocketServlet.java b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/AuthenticatingWebSocketServlet.java
new file mode 100644
index 000000000..a5795e08c
--- /dev/null
+++ b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/AuthenticatingWebSocketServlet.java
@@ -0,0 +1,161 @@
+
+package net.sourceforge.guacamole.net.basic.websocket.jetty;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import net.sourceforge.guacamole.GuacamoleException;
+import net.sourceforge.guacamole.GuacamoleServerException;
+import net.sourceforge.guacamole.net.auth.Credentials;
+import net.sourceforge.guacamole.net.auth.UserContext;
+import net.sourceforge.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 = 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);
+
+ }
+
+ /**
+ * Returns the credentials associated with the given session.
+ *
+ * @param session The session to retrieve credentials from.
+ * @return The credentials associated with the given session.
+ */
+ protected Credentials getCredentials(HttpSession session) {
+ return (Credentials) session.getAttribute(
+ AuthenticatingHttpServlet.CREDENTIALS_ATTRIBUTE);
+ }
+
+ /**
+ * Returns the UserContext associated with the given session.
+ *
+ * @param session The session to retrieve UserContext from.
+ * @return The UserContext associated with the given session.
+ */
+ protected UserContext getUserContext(HttpSession session) {
+ return (UserContext) session.getAttribute(
+ AuthenticatingHttpServlet.CONTEXT_ATTRIBUTE);
+ }
+
+
+ /**
+ * Function called after the credentials given in the request (if any)
+ * are authenticated. If the current session is not associated with
+ * valid credentials, this function will not be called.
+ *
+ * @param context The current UserContext.
+ * @param request The HttpServletRequest being serviced.
+ * @param protocol The protocol being used over the WebSocket connection.
+ */
+ protected abstract WebSocket authenticatedConnect(
+ UserContext context,
+ HttpServletRequest request, String protocol);
+
+}
diff --git a/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/BasicGuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/BasicGuacamoleWebSocketTunnelServlet.java
new file mode 100644
index 000000000..3e0d03ca2
--- /dev/null
+++ b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/BasicGuacamoleWebSocketTunnelServlet.java
@@ -0,0 +1,243 @@
+package net.sourceforge.guacamole.net.basic.websocket.jetty;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import net.sourceforge.guacamole.GuacamoleException;
+import net.sourceforge.guacamole.GuacamoleSecurityException;
+import net.sourceforge.guacamole.net.GuacamoleSocket;
+import net.sourceforge.guacamole.net.GuacamoleTunnel;
+import net.sourceforge.guacamole.net.auth.Connection;
+import net.sourceforge.guacamole.net.auth.Credentials;
+import net.sourceforge.guacamole.net.auth.Directory;
+import net.sourceforge.guacamole.net.auth.UserContext;
+import net.sourceforge.guacamole.net.basic.event.SessionListenerCollection;
+import net.sourceforge.guacamole.net.event.TunnelCloseEvent;
+import net.sourceforge.guacamole.net.event.TunnelConnectEvent;
+import net.sourceforge.guacamole.net.event.listener.TunnelCloseListener;
+import net.sourceforge.guacamole.net.event.listener.TunnelConnectListener;
+import net.sourceforge.guacamole.protocol.GuacamoleClientInformation;
+import org.eclipse.jetty.websocket.WebSocket;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Authenticating tunnel servlet implementation which uses WebSocket as a
+ * tunnel backend, rather than HTTP.
+ */
+public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet {
+
+ /**
+ * Logger for this class.
+ */
+ private Logger logger = LoggerFactory.getLogger(BasicGuacamoleWebSocketTunnelServlet.class);
+
+ /**
+ * 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 credentials The credentials associated with the authentication
+ * request that connected the tunnel.
+ * @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 boolean notifyConnect(Collection listeners,
+ Credentials credentials, GuacamoleTunnel tunnel)
+ throws GuacamoleException {
+
+ // Build event for auth success
+ TunnelConnectEvent event = new TunnelConnectEvent(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 credentials The credentials associated with the authentication
+ * request that closed the tunnel.
+ * @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 boolean notifyClose(Collection listeners,
+ Credentials credentials, GuacamoleTunnel tunnel)
+ throws GuacamoleException {
+
+ // Build event for auth success
+ TunnelCloseEvent event = new TunnelCloseEvent(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;
+
+ }
+
+ /**
+ * Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
+ * requests.
+ */
+ private GuacamoleWebSocketTunnelServlet tunnelServlet =
+ new GuacamoleWebSocketTunnelServlet() {
+
+ @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");
+
+ // Get credentials
+ final Credentials credentials = getCredentials(httpSession);
+
+ // Get context
+ UserContext context = getUserContext(httpSession);
+
+ // Get connection directory
+ Directory directory = context.getConnectionDirectory();
+
+ // If no credentials in session, not authorized
+ if (credentials == null)
+ throw new GuacamoleSecurityException("Cannot connect - user not logged in.");
+
+ // 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.");
+ }
+
+ logger.info("Successful connection from {} to \"{}\".", request.getRemoteAddr(), id);
+
+ // 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));
+
+ // Connect socket
+ GuacamoleSocket socket = connection.connect(info);
+
+ // Associate socket with tunnel
+ GuacamoleTunnel tunnel = new GuacamoleTunnel(socket) {
+
+ @Override
+ public void close() throws GuacamoleException {
+
+ // Only close if not canceled
+ if (!notifyClose(listeners, 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, credentials, tunnel)) {
+ logger.info("Connection canceled by listener.");
+ return null;
+ }
+
+ return tunnel;
+
+ }
+
+ };
+
+
+ @Override
+ protected WebSocket authenticatedConnect(UserContext context,
+ HttpServletRequest request, String protocol) {
+ return tunnelServlet.doWebSocketConnect(request, protocol);
+ }
+
+}
+
diff --git a/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/GuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/GuacamoleWebSocketTunnelServlet.java
new file mode 100644
index 000000000..01b659217
--- /dev/null
+++ b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/GuacamoleWebSocketTunnelServlet.java
@@ -0,0 +1,133 @@
+package net.sourceforge.guacamole.net.basic.websocket.jetty;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import net.sourceforge.guacamole.GuacamoleException;
+import net.sourceforge.guacamole.io.GuacamoleReader;
+import net.sourceforge.guacamole.io.GuacamoleWriter;
+import net.sourceforge.guacamole.net.GuacamoleTunnel;
+import org.eclipse.jetty.websocket.WebSocket;
+import org.eclipse.jetty.websocket.WebSocket.Connection;
+import org.eclipse.jetty.websocket.WebSocketServlet;
+
+/**
+ * A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet.
+ *
+ * @author Michael Jumper
+ */
+public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
+
+ /**
+ * The default, minimum buffer size for instructions.
+ */
+ private static final int BUFFER_SIZE = 8192;
+
+ @Override
+ public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
+
+ // Get tunnel
+ final GuacamoleTunnel tunnel;
+
+ try {
+ tunnel = doConnect(request);
+ }
+ catch (GuacamoleException e) {
+ return null; // FIXME: Can throw exception?
+ }
+
+ // Return new WebSocket which communicates through tunnel
+ return new WebSocket.OnTextMessage() {
+
+ @Override
+ public void onMessage(String string) {
+ GuacamoleWriter writer = tunnel.acquireWriter();
+
+ // Write message received
+ try {
+ writer.write(string.toCharArray());
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+
+ tunnel.releaseWriter();
+ }
+
+ @Override
+ public void onOpen(final Connection connection) {
+
+ Thread readThread = new Thread() {
+
+ @Override
+ public void run() {
+
+ StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
+ GuacamoleReader reader = tunnel.acquireReader();
+ char[] readMessage;
+
+ try {
+ while ((readMessage = reader.read()) != null) {
+
+ // Buffer message
+ buffer.append(readMessage);
+
+ // Flush if we expect to wait or buffer is getting full
+ if (!reader.available() || buffer.length() >= BUFFER_SIZE) {
+ connection.sendMessage(buffer.toString());
+ buffer.setLength(0);
+ }
+
+ }
+ }
+ catch (IOException e) {
+ // FIXME: Handle exception
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+
+ }
+
+ };
+
+ readThread.start();
+
+ }
+
+ @Override
+ public void onClose(int i, String string) {
+ try {
+ tunnel.close();
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+ }
+
+ };
+
+ }
+
+ protected abstract GuacamoleTunnel doConnect(HttpServletRequest request)
+ throws GuacamoleException;
+
+}
+
diff --git a/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/package-info.java b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/package-info.java
new file mode 100644
index 000000000..8ea925cfb
--- /dev/null
+++ b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/package-info.java
@@ -0,0 +1,8 @@
+
+/**
+ * Jetty WebSocket tunnel implementation. The classes here require at least
+ * Jetty 8, and may change significantly as there is no common WebSocket
+ * API for Java yet.
+ */
+package net.sourceforge.guacamole.net.basic.websocket.jetty;
+
diff --git a/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/AuthenticatingWebSocketServlet.java b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/AuthenticatingWebSocketServlet.java
new file mode 100644
index 000000000..480e20382
--- /dev/null
+++ b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/AuthenticatingWebSocketServlet.java
@@ -0,0 +1,161 @@
+
+package net.sourceforge.guacamole.net.basic.websocket.tomcat;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import net.sourceforge.guacamole.GuacamoleException;
+import net.sourceforge.guacamole.GuacamoleServerException;
+import net.sourceforge.guacamole.net.auth.Credentials;
+import net.sourceforge.guacamole.net.auth.UserContext;
+import net.sourceforge.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 = 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);
+
+ }
+
+ /**
+ * Returns the credentials associated with the given session.
+ *
+ * @param session The session to retrieve credentials from.
+ * @return The credentials associated with the given session.
+ */
+ protected Credentials getCredentials(HttpSession session) {
+ return (Credentials) session.getAttribute(
+ AuthenticatingHttpServlet.CREDENTIALS_ATTRIBUTE);
+ }
+
+ /**
+ * Returns the UserContext associated with the given session.
+ *
+ * @param session The session to retrieve UserContext from.
+ * @return The UserContext associated with the given session.
+ */
+ protected UserContext getUserContext(HttpSession session) {
+ return (UserContext) session.getAttribute(
+ AuthenticatingHttpServlet.CONTEXT_ATTRIBUTE);
+ }
+
+
+ /**
+ * Function called after the credentials given in the request (if any)
+ * are authenticated. If the current session is not associated with
+ * valid credentials, this function will not be called.
+ *
+ * @param context The current UserContext.
+ * @param request The HttpServletRequest being serviced.
+ * @param protocol The protocol being used over the WebSocket connection.
+ */
+ protected abstract StreamInbound authenticatedConnect(
+ UserContext context,
+ HttpServletRequest request, String protocol);
+
+}
diff --git a/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java
new file mode 100644
index 000000000..e4db7b346
--- /dev/null
+++ b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java
@@ -0,0 +1,243 @@
+package net.sourceforge.guacamole.net.basic.websocket.tomcat;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import net.sourceforge.guacamole.GuacamoleException;
+import net.sourceforge.guacamole.GuacamoleSecurityException;
+import net.sourceforge.guacamole.net.GuacamoleSocket;
+import net.sourceforge.guacamole.net.GuacamoleTunnel;
+import net.sourceforge.guacamole.net.auth.Connection;
+import net.sourceforge.guacamole.net.auth.Credentials;
+import net.sourceforge.guacamole.net.auth.Directory;
+import net.sourceforge.guacamole.net.auth.UserContext;
+import net.sourceforge.guacamole.net.basic.event.SessionListenerCollection;
+import net.sourceforge.guacamole.net.event.TunnelCloseEvent;
+import net.sourceforge.guacamole.net.event.TunnelConnectEvent;
+import net.sourceforge.guacamole.net.event.listener.TunnelCloseListener;
+import net.sourceforge.guacamole.net.event.listener.TunnelConnectListener;
+import net.sourceforge.guacamole.protocol.GuacamoleClientInformation;
+import org.apache.catalina.websocket.StreamInbound;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Authenticating tunnel servlet implementation which uses WebSocket as a
+ * tunnel backend, rather than HTTP.
+ */
+public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet {
+
+ /**
+ * Logger for this class.
+ */
+ private Logger logger = LoggerFactory.getLogger(BasicGuacamoleWebSocketTunnelServlet.class);
+
+ /**
+ * 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 credentials The credentials associated with the authentication
+ * request that connected the tunnel.
+ * @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 boolean notifyConnect(Collection listeners,
+ Credentials credentials, GuacamoleTunnel tunnel)
+ throws GuacamoleException {
+
+ // Build event for auth success
+ TunnelConnectEvent event = new TunnelConnectEvent(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 credentials The credentials associated with the authentication
+ * request that closed the tunnel.
+ * @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 boolean notifyClose(Collection listeners,
+ Credentials credentials, GuacamoleTunnel tunnel)
+ throws GuacamoleException {
+
+ // Build event for auth success
+ TunnelCloseEvent event = new TunnelCloseEvent(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;
+
+ }
+
+ /**
+ * Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
+ * requests.
+ */
+ private GuacamoleWebSocketTunnelServlet tunnelServlet =
+ new GuacamoleWebSocketTunnelServlet() {
+
+ @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");
+
+ // Get credentials
+ final Credentials credentials = getCredentials(httpSession);
+
+ // Get context
+ UserContext context = getUserContext(httpSession);
+
+ // Get connection directory
+ Directory directory = context.getConnectionDirectory();
+
+ // If no credentials in session, not authorized
+ if (credentials == null)
+ throw new GuacamoleSecurityException("Cannot connect - user not logged in.");
+
+ // 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.");
+ }
+
+ logger.info("Successful connection from {} to \"{}\".", request.getRemoteAddr(), id);
+
+ // 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));
+
+ // Connect socket
+ GuacamoleSocket socket = connection.connect(info);
+
+ // Associate socket with tunnel
+ GuacamoleTunnel tunnel = new GuacamoleTunnel(socket) {
+
+ @Override
+ public void close() throws GuacamoleException {
+
+ // Only close if not canceled
+ if (!notifyClose(listeners, 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, credentials, tunnel)) {
+ logger.info("Connection canceled by listener.");
+ return null;
+ }
+
+ return tunnel;
+
+ }
+
+ };
+
+
+ @Override
+ protected StreamInbound authenticatedConnect(UserContext context,
+ HttpServletRequest request, String protocol) {
+ return tunnelServlet.createWebSocketInbound(protocol, request);
+ }
+
+}
+
diff --git a/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java
new file mode 100644
index 000000000..7363d275b
--- /dev/null
+++ b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java
@@ -0,0 +1,166 @@
+package net.sourceforge.guacamole.net.basic.websocket.tomcat;
+
+/*
+ * Guacamole - Clientless Remote Desktop
+ * Copyright (C) 2010 Michael Jumper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.CharBuffer;
+import javax.servlet.http.HttpServletRequest;
+import net.sourceforge.guacamole.GuacamoleException;
+import net.sourceforge.guacamole.io.GuacamoleReader;
+import net.sourceforge.guacamole.io.GuacamoleWriter;
+import net.sourceforge.guacamole.net.GuacamoleTunnel;
+import org.apache.catalina.websocket.StreamInbound;
+import org.apache.catalina.websocket.WebSocketServlet;
+import org.apache.catalina.websocket.WsOutbound;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet.
+ *
+ * @author Michael Jumper
+ */
+public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
+
+ /**
+ * The default, minimum buffer size for instructions.
+ */
+ private static final int BUFFER_SIZE = 8192;
+
+ /**
+ * Logger for this class.
+ */
+ private Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelServlet.class);
+
+ @Override
+ public StreamInbound createWebSocketInbound(String protocol, HttpServletRequest request) {
+
+ // Get tunnel
+ final GuacamoleTunnel tunnel;
+
+ try {
+ tunnel = doConnect(request);
+ }
+ catch (GuacamoleException e) {
+ logger.error("Error connecting WebSocket tunnel.", e);
+ return null;
+ }
+
+ // Return new WebSocket which communicates through tunnel
+ return new StreamInbound() {
+
+ @Override
+ protected void onTextData(Reader reader) throws IOException {
+
+ GuacamoleWriter writer = tunnel.acquireWriter();
+
+ // Write all available data
+ try {
+
+ char[] buffer = new char[BUFFER_SIZE];
+
+ int num_read;
+ while ((num_read = reader.read(buffer)) > 0)
+ writer.write(buffer, 0, num_read);
+
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+
+ tunnel.releaseWriter();
+ }
+
+ @Override
+ public void onOpen(final WsOutbound outbound) {
+
+ Thread readThread = new Thread() {
+
+ @Override
+ public void run() {
+
+ CharBuffer charBuffer = CharBuffer.allocate(BUFFER_SIZE);
+ StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
+ GuacamoleReader reader = tunnel.acquireReader();
+ char[] readMessage;
+
+ try {
+ while ((readMessage = reader.read()) != null) {
+
+ // Buffer message
+ buffer.append(readMessage);
+
+ // Flush if we expect to wait or buffer is getting full
+ if (!reader.available() || buffer.length() >= BUFFER_SIZE) {
+
+ // Reallocate buffer if necessary
+ if (buffer.length() > charBuffer.length())
+ charBuffer = CharBuffer.allocate(buffer.length());
+ else
+ charBuffer.clear();
+
+ charBuffer.put(buffer.toString().toCharArray());
+ charBuffer.flip();
+
+ outbound.writeTextMessage(charBuffer);
+ buffer.setLength(0);
+ }
+
+ }
+ }
+ catch (IOException e) {
+ // FIXME: Handle exception
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+
+ }
+
+ };
+
+ readThread.start();
+
+ }
+
+ @Override
+ public void onClose(int i) {
+ try {
+ tunnel.close();
+ }
+ catch (GuacamoleException e) {
+ // FIXME: Handle exception
+ }
+ }
+
+ @Override
+ protected void onBinaryData(InputStream in) throws IOException {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ };
+
+ }
+
+ protected abstract GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException;
+
+}
+
diff --git a/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/package-info.java b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/package-info.java
new file mode 100644
index 000000000..d08f76df2
--- /dev/null
+++ b/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/package-info.java
@@ -0,0 +1,8 @@
+
+/**
+ * Tomcat WebSocket tunnel implementation. The classes here require at least
+ * Tomcat 7.0, and may change significantly as there is no common WebSocket
+ * API for Java yet.
+ */
+package net.sourceforge.guacamole.net.basic.websocket.tomcat;
+
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingHttpServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingHttpServlet.java
index f42d88b19..41d80ca6b 100644
--- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingHttpServlet.java
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/AuthenticatingHttpServlet.java
@@ -70,12 +70,12 @@ public abstract class AuthenticatingHttpServlet extends HttpServlet {
/**
* The session attribute holding the current UserContext.
*/
- private static final String CONTEXT_ATTRIBUTE = "GUAC_CONTEXT";
+ public static final String CONTEXT_ATTRIBUTE = "GUAC_CONTEXT";
/**
* The session attribute holding the credentials authorizing this session.
*/
- private static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS";
+ public static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS";
/**
* The AuthenticationProvider to use to authenticate all requests.
diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/WebSocketSupportLoader.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/WebSocketSupportLoader.java
index ecf88c24b..43ccd0cc6 100644
--- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/WebSocketSupportLoader.java
+++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/WebSocketSupportLoader.java
@@ -46,19 +46,25 @@ public class WebSocketSupportLoader implements ServletContextListener {
*/
private Logger logger = LoggerFactory.getLogger(WebSocketSupportLoader.class);
- @Override
- public void contextDestroyed(ServletContextEvent sce) {
- }
+ /**
+ * Classname of the Jetty-specific WebSocket tunnel implementation.
+ */
+ private static final String JETTY_WEBSOCKET =
+ "net.sourceforge.guacamole.net.basic.websocket.jetty.BasicGuacamoleWebSocketTunnelServlet";
- @Override
- public void contextInitialized(ServletContextEvent sce) {
+ /**
+ * Classname of the Tomcat-specific WebSocket tunnel implementation.
+ */
+ private static final String TOMCAT_WEBSOCKET =
+ "net.sourceforge.guacamole.net.basic.websocket.tomcat.BasicGuacamoleWebSocketTunnelServlet";
+
+ private boolean loadWebSocketTunnel(ServletContext context, String classname) {
try {
// Attempt to find WebSocket servlet
- Class servlet = (Class) GuacamoleClassLoader.getInstance().findClass(
- "org.glyptodon.guacamole.net.basic.BasicGuacamoleWebSocketTunnelServlet"
- );
+ Class servlet = (Class)
+ GuacamoleClassLoader.getInstance().findClass(classname);
// Dynamically add servlet IF SERVLET 3.0 API AVAILABLE!
try {
@@ -67,8 +73,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 +84,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;
}
@@ -100,7 +108,10 @@ public class WebSocketSupportLoader implements ServletContextListener {
// If no such servlet class, WebSocket support not present
catch (ClassNotFoundException e) {
- logger.info("WebSocket support not found.");
+ logger.info("WebSocket support not found.", e);
+ }
+ catch (NoClassDefFoundError e) {
+ logger.info("WebSocket support not found.", e);
}
// Log all GuacamoleExceptions
@@ -108,6 +119,31 @@ public class WebSocketSupportLoader implements ServletContextListener {
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 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.");
+
}
}