diff --git a/guacamole/pom.xml b/guacamole/pom.xml index 9e8f6ae44..01d075359 100644 --- a/guacamole/pom.xml +++ b/guacamole/pom.xml @@ -142,7 +142,7 @@ runtime - + org.eclipse.jetty jetty-websocket diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketSupportLoader.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketSupportLoader.java index 103add2f0..723f685cb 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketSupportLoader.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketSupportLoader.java @@ -30,8 +30,6 @@ import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader; -import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty; -import org.glyptodon.guacamole.properties.GuacamoleProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,17 +42,6 @@ import org.slf4j.LoggerFactory; * 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 */ public class WebSocketSupportLoader implements ServletContextListener { @@ -65,16 +52,12 @@ public class WebSocketSupportLoader implements ServletContextListener { private final Logger logger = LoggerFactory.getLogger(WebSocketSupportLoader.class); /** - * Classname of the Jetty-specific WebSocket tunnel implementation. + * Classnames of all legacy (non-JSR) WebSocket tunnel implementations. */ - 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 static final String[] WEBSOCKET_CLASSES = { + "org.glyptodon.guacamole.net.basic.websocket.jetty8.BasicGuacamoleWebSocketTunnelServlet", + "org.glyptodon.guacamole.net.basic.websocket.tomcat.BasicGuacamoleWebSocketTunnelServlet" + }; private boolean loadWebSocketTunnel(ServletContext context, String classname) { @@ -101,7 +84,6 @@ 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; } @@ -146,12 +128,16 @@ public class WebSocketSupportLoader implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { - // Try to load websocket support for Jetty - if (loadWebSocketTunnel(sce.getServletContext(), JETTY_WEBSOCKET)) - return; + // Try to load each WebSocket tunnel in sequence + for (String classname : WEBSOCKET_CLASSES) { + if (loadWebSocketTunnel(sce.getServletContext(), classname)) { + logger.info("Legacy (non-JSR) WebSocket support loaded: {}", classname); + return; + } + } - // Failing that, try to load websocket support for Tomcat - loadWebSocketTunnel(sce.getServletContext(), TOMCAT_WEBSOCKET); + // No legacy WebSocket support found (usually good) + logger.debug("Legacy WebSocket support NOT loaded."); } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/BasicGuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/BasicGuacamoleWebSocketTunnelServlet.java new file mode 100644 index 000000000..f9e6106a3 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/BasicGuacamoleWebSocketTunnelServlet.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2013 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.websocket.jetty8; + +import javax.servlet.http.HttpServletRequest; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.GuacamoleTunnel; +import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility; +import org.glyptodon.guacamole.net.basic.HTTPTunnelRequest; + +/** + * Tunnel servlet implementation which uses WebSocket as a tunnel backend, + * rather than HTTP, properly parsing connection IDs included in the connection + * request. + */ +public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet { + + @Override + protected GuacamoleTunnel doConnect(HttpServletRequest request) + throws GuacamoleException { + return BasicTunnelRequestUtility.createTunnel(new HTTPTunnelRequest(request)); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/GuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/GuacamoleWebSocketTunnelServlet.java new file mode 100644 index 000000000..85a11741b --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/GuacamoleWebSocketTunnelServlet.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2013 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.websocket.jetty8; + +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; +import org.glyptodon.guacamole.GuacamoleClientException; +import org.glyptodon.guacamole.protocol.GuacamoleStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet. + * + * @author Michael Jumper + */ +public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelServlet.class); + + /** + * The default, minimum buffer size for instructions. + */ + private static final int BUFFER_SIZE = 8192; + + /** + * Sends the given status on the given WebSocket connection and closes the + * connection. + * + * @param connection The WebSocket connection to close. + * @param guac_status The status to send. + */ + public static void closeConnection(Connection connection, + GuacamoleStatus guac_status) { + + connection.close(guac_status.getWebSocketCode(), + Integer.toString(guac_status.getGuacamoleStatusCode())); + + } + + @Override + public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { + + // 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 WebSocket.OnTextMessage() { + + @Override + public void onMessage(String string) { + GuacamoleWriter writer = tunnel.acquireWriter(); + + // Write message received + try { + writer.write(string.toCharArray()); + } + catch (GuacamoleException e) { + logger.debug("Tunnel write failed.", e); + } + + 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 { + + try { + + // Attempt to read + 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); + } + + } + + // No more data + closeConnection(connection, GuacamoleStatus.SUCCESS); + + } + + // Catch any thrown guacamole exception and attempt + // to pass within the WebSocket connection, logging + // each error appropriately. + catch (GuacamoleClientException e) { + logger.warn("Client request rejected: {}", e.getMessage()); + closeConnection(connection, e.getStatus()); + } + catch (GuacamoleException e) { + logger.error("Internal server error.", e); + closeConnection(connection, e.getStatus()); + } + + } + catch (IOException e) { + logger.debug("Tunnel read failed due to I/O error.", e); + } + + } + + }; + + readThread.start(); + + } + + @Override + public void onClose(int i, String string) { + try { + tunnel.close(); + } + catch (GuacamoleException e) { + logger.debug("Unable to close WebSocket tunnel.", e); + } + } + + }; + + } + + protected abstract GuacamoleTunnel doConnect(HttpServletRequest request) + throws GuacamoleException; + +} + diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/package-info.java new file mode 100644 index 000000000..9b63a7d49 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2013 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Jetty 8 WebSocket tunnel implementation. The classes here require Jetty 8. + */ +package org.glyptodon.guacamole.net.basic.websocket.jetty8; +