From 4599ac743148406d9e8797af5727069d3363859f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 14 Mar 2013 22:33:45 -0700 Subject: [PATCH] Add official WebSocket support, always enabled, for both Jetty and Tomcat. --- guacamole/pom.xml | 23 ++ .../jetty/AuthenticatingWebSocketServlet.java | 161 ++++++++++++ .../BasicGuacamoleWebSocketTunnelServlet.java | 243 ++++++++++++++++++ .../GuacamoleWebSocketTunnelServlet.java | 133 ++++++++++ .../basic/websocket/jetty/package-info.java | 8 + .../AuthenticatingWebSocketServlet.java | 161 ++++++++++++ .../BasicGuacamoleWebSocketTunnelServlet.java | 243 ++++++++++++++++++ .../GuacamoleWebSocketTunnelServlet.java | 166 ++++++++++++ .../basic/websocket/tomcat/package-info.java | 8 + .../net/basic/AuthenticatingHttpServlet.java | 4 +- .../net/basic/WebSocketSupportLoader.java | 58 ++++- 11 files changed, 1195 insertions(+), 13 deletions(-) create mode 100644 guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/AuthenticatingWebSocketServlet.java create mode 100644 guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/BasicGuacamoleWebSocketTunnelServlet.java create mode 100644 guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/GuacamoleWebSocketTunnelServlet.java create mode 100644 guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/jetty/package-info.java create mode 100644 guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/AuthenticatingWebSocketServlet.java create mode 100644 guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.java create mode 100644 guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java create mode 100644 guacamole/src/main/java/net/sourceforge/guacamole/net/basic/websocket/tomcat/package-info.java 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."); + } }