GUACAMOLE-567: Add support for WebSocket-specific ping messages to the legacy WebSocket tunnel implementations.
Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/819d3178 Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/819d3178 Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/819d3178 Branch: refs/heads/master Commit: 819d3178343a84af6926d862dc9cf584f9aa80f1 Parents: ea0b33b Author: Michael Jumper <mjum...@apache.org> Authored: Thu Sep 6 19:49:02 2018 -0700 Committer: Michael Jumper <mjum...@apache.org> Committed: Fri Sep 7 12:20:28 2018 -0700 ---------------------------------------------------------------------- .../jetty8/GuacamoleWebSocketTunnelServlet.java | 111 ++++++++++++++++- .../GuacamoleWebSocketTunnelListener.java | 121 +++++++++++++++++-- .../tomcat/GuacamoleWebSocketTunnelServlet.java | 110 ++++++++++++++++- 3 files changed, 323 insertions(+), 19 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/819d3178/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty8/GuacamoleWebSocketTunnelServlet.java ---------------------------------------------------------------------- diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty8/GuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty8/GuacamoleWebSocketTunnelServlet.java index e5c5db9..304e1dd 100644 --- a/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty8/GuacamoleWebSocketTunnelServlet.java +++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty8/GuacamoleWebSocketTunnelServlet.java @@ -20,6 +20,7 @@ package org.apache.guacamole.tunnel.websocket.jetty8; import java.io.IOException; +import java.util.List; import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.io.GuacamoleReader; @@ -30,6 +31,8 @@ import org.eclipse.jetty.websocket.WebSocket.Connection; import org.eclipse.jetty.websocket.WebSocketServlet; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleConnectionClosedException; +import org.apache.guacamole.protocol.FilteredGuacamoleWriter; +import org.apache.guacamole.protocol.GuacamoleFilter; import org.apache.guacamole.protocol.GuacamoleInstruction; import org.apache.guacamole.tunnel.http.HTTPTunnelRequest; import org.apache.guacamole.tunnel.TunnelRequest; @@ -53,6 +56,15 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { private static final int BUFFER_SIZE = 8192; /** + * The opcode of the instruction used to indicate a connection stability + * test ping request or response. Note that this instruction is + * encapsulated within an internal tunnel instruction (with the opcode + * being the empty string), thus this will actually be the value of the + * first element of the received instruction. + */ + private static final String PING_OPCODE = "ping"; + + /** * Sends the given numeric Guacamole and WebSocket status * on the given WebSocket connection and closes the * connection. @@ -106,6 +118,58 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { */ private GuacamoleTunnel tunnel = null; + /** + * The active WebSocket connection. This value will always be + * non-null if tunnel is non-null. + */ + private Connection connection = null; + + /** + * Sends a Guacamole instruction along the outbound WebSocket + * connection to the connected Guacamole client. If an instruction + * is already in the process of being sent by another thread, this + * function will block until in-progress instructions are complete. + * + * @param instruction + * The instruction to send. + * + * @throws IOException + * If an I/O error occurs preventing the given instruction from + * being sent. + */ + private void sendInstruction(String instruction) + throws IOException { + + // NOTE: Synchronization on the non-final remote field here is + // intentional. The outbound websocket connection is only + // sensitive to simultaneous attempts to send messages with + // respect to itself. If the connection changes, then + // synchronization need only be performed in context of the new + // connection + synchronized (connection) { + connection.sendMessage(instruction); + } + + } + + /** + * Sends a Guacamole instruction along the outbound WebSocket + * connection to the connected Guacamole client. If an instruction + * is already in the process of being sent by another thread, this + * function will block until in-progress instructions are complete. + * + * @param instruction + * The instruction to send. + * + * @throws IOException + * If an I/O error occurs preventing the given instruction from being + * sent. + */ + private void sendInstruction(GuacamoleInstruction instruction) + throws IOException { + sendInstruction(instruction.toString()); + } + @Override public void onMessage(String string) { @@ -113,7 +177,43 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { if (tunnel == null) return; - GuacamoleWriter writer = tunnel.acquireWriter(); + // Filter received instructions, handling tunnel-internal + // instructions without passing through to guacd + GuacamoleWriter writer = new FilteredGuacamoleWriter(tunnel.acquireWriter(), new GuacamoleFilter() { + + @Override + public GuacamoleInstruction filter(GuacamoleInstruction instruction) + throws GuacamoleException { + + // Filter out all tunnel-internal instructions + if (instruction.getOpcode().equals(GuacamoleTunnel.INTERNAL_DATA_OPCODE)) { + + // Respond to ping requests + List<String> args = instruction.getArgs(); + if (args.size() >= 2 && args.get(0).equals(PING_OPCODE)) { + + try { + sendInstruction(new GuacamoleInstruction( + GuacamoleTunnel.INTERNAL_DATA_OPCODE, + PING_OPCODE, args.get(1) + )); + } + catch (IOException e) { + logger.debug("Unable to send \"ping\" response for WebSocket tunnel.", e); + } + + } + + return null; + + } + + // Pass through all non-internal instructions untouched + return instruction; + + } + + }); // Write message received try { @@ -133,6 +233,9 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { @Override public void onOpen(final Connection connection) { + // Store websocket connection for future use via sendInstruction() + this.connection = connection; + try { tunnel = doConnect(tunnelRequest); } @@ -162,10 +265,10 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { try { // Send tunnel UUID - connection.sendMessage(new GuacamoleInstruction( + sendInstruction(new GuacamoleInstruction( GuacamoleTunnel.INTERNAL_DATA_OPCODE, tunnel.getUUID().toString() - ).toString()); + )); try { @@ -177,7 +280,7 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { // Flush if we expect to wait or buffer is getting full if (!reader.available() || buffer.length() >= BUFFER_SIZE) { - connection.sendMessage(buffer.toString()); + sendInstruction(buffer.toString()); buffer.setLength(0); } http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/819d3178/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty9/GuacamoleWebSocketTunnelListener.java ---------------------------------------------------------------------- diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty9/GuacamoleWebSocketTunnelListener.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty9/GuacamoleWebSocketTunnelListener.java index 0594d06..6422f57 100644 --- a/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty9/GuacamoleWebSocketTunnelListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/jetty9/GuacamoleWebSocketTunnelListener.java @@ -20,6 +20,7 @@ package org.apache.guacamole.tunnel.websocket.jetty9; import java.io.IOException; +import java.util.List; import org.eclipse.jetty.websocket.api.CloseStatus; import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; @@ -30,6 +31,8 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.io.GuacamoleReader; import org.apache.guacamole.io.GuacamoleWriter; import org.apache.guacamole.net.GuacamoleTunnel; +import org.apache.guacamole.protocol.FilteredGuacamoleWriter; +import org.apache.guacamole.protocol.GuacamoleFilter; import org.apache.guacamole.protocol.GuacamoleInstruction; import org.apache.guacamole.protocol.GuacamoleStatus; import org.slf4j.Logger; @@ -46,16 +49,32 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe private static final int BUFFER_SIZE = 8192; /** + * The opcode of the instruction used to indicate a connection stability + * test ping request or response. Note that this instruction is + * encapsulated within an internal tunnel instruction (with the opcode + * being the empty string), thus this will actually be the value of the + * first element of the received instruction. + */ + private static final String PING_OPCODE = "ping"; + + /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(RestrictedGuacamoleWebSocketTunnelServlet.class); /** * The underlying GuacamoleTunnel. WebSocket reads/writes will be handled - * as reads/writes to this tunnel. + * as reads/writes to this tunnel. This value may be null if no connection + * has been established. */ private GuacamoleTunnel tunnel; - + + /** + * Remote (client) side of this connection. This value will always be + * non-null if tunnel is non-null. + */ + private RemoteEndpoint remote; + /** * Sends the given numeric Guacamole and WebSocket status * codes on the given WebSocket connection and closes the @@ -102,6 +121,52 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe } /** + * Sends a Guacamole instruction along the outbound WebSocket connection to + * the connected Guacamole client. If an instruction is already in the + * process of being sent by another thread, this function will block until + * in-progress instructions are complete. + * + * @param instruction + * The instruction to send. + * + * @throws IOException + * If an I/O error occurs preventing the given instruction from being + * sent. + */ + private void sendInstruction(String instruction) + throws IOException { + + // NOTE: Synchronization on the non-final remote field here is + // intentional. The remote (the outbound websocket connection) is only + // sensitive to simultaneous attempts to send messages with respect to + // itself. If the remote changes, then the outbound websocket + // connection has changed, and synchronization need only be performed + // in context of the new remote. + synchronized (remote) { + remote.sendString(instruction); + } + + } + + /** + * Sends a Guacamole instruction along the outbound WebSocket connection to + * the connected Guacamole client. If an instruction is already in the + * process of being sent by another thread, this function will block until + * in-progress instructions are complete. + * + * @param instruction + * The instruction to send. + * + * @throws IOException + * If an I/O error occurs preventing the given instruction from being + * sent. + */ + private void sendInstruction(GuacamoleInstruction instruction) + throws IOException { + sendInstruction(instruction.toString()); + } + + /** * Returns a new tunnel for the given session. How this tunnel is created * or retrieved is implementation-dependent. * @@ -117,6 +182,9 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe @Override public void onWebSocketConnect(final Session session) { + // Store underlying remote for future use via sendInstruction() + remote = session.getRemote(); + try { // Get tunnel @@ -137,11 +205,6 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe // Prepare read transfer thread Thread readThread = new Thread() { - /** - * Remote (client) side of this connection - */ - private final RemoteEndpoint remote = session.getRemote(); - @Override public void run() { @@ -152,10 +215,10 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe try { // Send tunnel UUID - remote.sendString(new GuacamoleInstruction( + sendInstruction(new GuacamoleInstruction( GuacamoleTunnel.INTERNAL_DATA_OPCODE, tunnel.getUUID().toString() - ).toString()); + )); try { @@ -167,7 +230,7 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe // Flush if we expect to wait or buffer is getting full if (!reader.available() || buffer.length() >= BUFFER_SIZE) { - remote.sendString(buffer.toString()); + sendInstruction(buffer.toString()); buffer.setLength(0); } @@ -219,7 +282,43 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe if (tunnel == null) return; - GuacamoleWriter writer = tunnel.acquireWriter(); + // Filter received instructions, handling tunnel-internal instructions + // without passing through to guacd + GuacamoleWriter writer = new FilteredGuacamoleWriter(tunnel.acquireWriter(), new GuacamoleFilter() { + + @Override + public GuacamoleInstruction filter(GuacamoleInstruction instruction) + throws GuacamoleException { + + // Filter out all tunnel-internal instructions + if (instruction.getOpcode().equals(GuacamoleTunnel.INTERNAL_DATA_OPCODE)) { + + // Respond to ping requests + List<String> args = instruction.getArgs(); + if (args.size() >= 2 && args.get(0).equals(PING_OPCODE)) { + + try { + sendInstruction(new GuacamoleInstruction( + GuacamoleTunnel.INTERNAL_DATA_OPCODE, + PING_OPCODE, args.get(1) + )); + } + catch (IOException e) { + logger.debug("Unable to send \"ping\" response for WebSocket tunnel.", e); + } + + } + + return null; + + } + + // Pass through all non-internal instructions untouched + return instruction; + + } + + }); try { // Write received message http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/819d3178/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java ---------------------------------------------------------------------- diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java index a2e8b39..215cc8f 100644 --- a/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java +++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/websocket/tomcat/GuacamoleWebSocketTunnelServlet.java @@ -35,6 +35,8 @@ import org.apache.catalina.websocket.WebSocketServlet; import org.apache.catalina.websocket.WsOutbound; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleConnectionClosedException; +import org.apache.guacamole.protocol.FilteredGuacamoleWriter; +import org.apache.guacamole.protocol.GuacamoleFilter; import org.apache.guacamole.protocol.GuacamoleInstruction; import org.apache.guacamole.tunnel.http.HTTPTunnelRequest; import org.apache.guacamole.tunnel.TunnelRequest; @@ -53,6 +55,15 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { private static final int BUFFER_SIZE = 8192; /** + * The opcode of the instruction used to indicate a connection stability + * test ping request or response. Note that this instruction is + * encapsulated within an internal tunnel instruction (with the opcode + * being the empty string), thus this will actually be the value of the + * first element of the received instruction. + */ + private static final String PING_OPCODE = "ping"; + + /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelServlet.class); @@ -130,6 +141,58 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { */ private GuacamoleTunnel tunnel = null; + /** + * The outbound half of the WebSocket connection. This value will + * always be non-null if tunnel is non-null. + */ + private WsOutbound outbound = null; + + /** + * Sends a Guacamole instruction along the outbound WebSocket + * connection to the connected Guacamole client. If an instruction + * is already in the process of being sent by another thread, this + * function will block until in-progress instructions are complete. + * + * @param instruction + * The instruction to send. + * + * @throws IOException + * If an I/O error occurs preventing the given instruction from + * being sent. + */ + private void sendInstruction(CharSequence instruction) + throws IOException { + + // NOTE: Synchronization on the non-final remote field here is + // intentional. The outbound websocket connection is only + // sensitive to simultaneous attempts to send messages with + // respect to itself. If the connection changes, then + // synchronization need only be performed in context of the new + // connection + synchronized (outbound) { + outbound.writeTextMessage(CharBuffer.wrap(instruction)); + } + + } + + /** + * Sends a Guacamole instruction along the outbound WebSocket + * connection to the connected Guacamole client. If an instruction + * is already in the process of being sent by another thread, this + * function will block until in-progress instructions are complete. + * + * @param instruction + * The instruction to send. + * + * @throws IOException + * If an I/O error occurs preventing the given instruction from being + * sent. + */ + private void sendInstruction(GuacamoleInstruction instruction) + throws IOException { + sendInstruction(instruction.toString()); + } + @Override protected void onTextData(Reader reader) throws IOException { @@ -137,7 +200,43 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { if (tunnel == null) return; - GuacamoleWriter writer = tunnel.acquireWriter(); + // Filter received instructions, handling tunnel-internal + // instructions without passing through to guacd + GuacamoleWriter writer = new FilteredGuacamoleWriter(tunnel.acquireWriter(), new GuacamoleFilter() { + + @Override + public GuacamoleInstruction filter(GuacamoleInstruction instruction) + throws GuacamoleException { + + // Filter out all tunnel-internal instructions + if (instruction.getOpcode().equals(GuacamoleTunnel.INTERNAL_DATA_OPCODE)) { + + // Respond to ping requests + List<String> args = instruction.getArgs(); + if (args.size() >= 2 && args.get(0).equals(PING_OPCODE)) { + + try { + sendInstruction(new GuacamoleInstruction( + GuacamoleTunnel.INTERNAL_DATA_OPCODE, + PING_OPCODE, args.get(1) + )); + } + catch (IOException e) { + logger.debug("Unable to send \"ping\" response for WebSocket tunnel.", e); + } + + } + + return null; + + } + + // Pass through all non-internal instructions untouched + return instruction; + + } + + }); // Write all available data try { @@ -162,6 +261,9 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { @Override public void onOpen(final WsOutbound outbound) { + // Store outbound connection for future use via sendInstruction() + this.outbound = outbound; + try { tunnel = doConnect(tunnelRequest); } @@ -191,10 +293,10 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { try { // Send tunnel UUID - outbound.writeTextMessage(CharBuffer.wrap(new GuacamoleInstruction( + sendInstruction(new GuacamoleInstruction( GuacamoleTunnel.INTERNAL_DATA_OPCODE, tunnel.getUUID().toString() - ).toString())); + )); try { @@ -206,7 +308,7 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { // Flush if we expect to wait or buffer is getting full if (!reader.available() || buffer.length() >= BUFFER_SIZE) { - outbound.writeTextMessage(CharBuffer.wrap(buffer)); + sendInstruction(CharBuffer.wrap(buffer)); buffer.setLength(0); }