diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index 06cc741a3..827dd6cd4 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -153,7 +153,8 @@ Guacamole.Tunnel = function() { * use by tunnel implementations. The value of this opcode is guaranteed to be * the empty string (""). Tunnel implementations may use this opcode for any * purpose. It is currently used by the HTTP tunnel to mark the end of the HTTP - * response, and by the WebSocket tunnel to transmit the tunnel UUID. + * response, and by the WebSocket tunnel to transmit the tunnel UUID and send + * connection stability test pings/responses. * * @constant * @type {String} @@ -742,6 +743,15 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { */ var unstableTimeout = null; + /** + * The current connection stability test ping interval ID, if any. This + * will only be set upon successful connection. + * + * @private + * @type {Number} + */ + var pingInterval = null; + /** * The WebSocket protocol corresponding to the protocol used for the current * location. @@ -752,6 +762,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { "https:": "wss:" }; + /** + * The number of milliseconds to wait between connection stability test + * pings. + * + * @private + * @constant + * @type {Number} + */ + var PING_FREQUENCY = 500; + // Transform current URL to WebSocket URL // If not already a websocket URL @@ -828,6 +848,9 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { window.clearTimeout(receive_timeout); window.clearTimeout(unstableTimeout); + // Cease connection test pings + window.clearInterval(pingInterval); + // Ignore if already closed if (tunnel.state === Guacamole.Tunnel.State.CLOSED) return; @@ -892,6 +915,13 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { socket.onopen = function(event) { reset_timeout(); + + // Ping tunnel endpoint regularly to test connection stability + pingInterval = setInterval(function sendPing() { + tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, + "ping", new Date().getTime()); + }, PING_FREQUENCY); + }; socket.onclose = function(event) { diff --git a/guacamole-common/src/main/java/org/apache/guacamole/websocket/GuacamoleWebSocketTunnelEndpoint.java b/guacamole-common/src/main/java/org/apache/guacamole/websocket/GuacamoleWebSocketTunnelEndpoint.java index 0e0262254..772ce64b2 100644 --- a/guacamole-common/src/main/java/org/apache/guacamole/websocket/GuacamoleWebSocketTunnelEndpoint.java +++ b/guacamole-common/src/main/java/org/apache/guacamole/websocket/GuacamoleWebSocketTunnelEndpoint.java @@ -20,6 +20,7 @@ package org.apache.guacamole.websocket; import java.io.IOException; +import java.util.List; import javax.websocket.CloseReason; import javax.websocket.CloseReason.CloseCode; import javax.websocket.Endpoint; @@ -36,6 +37,8 @@ import org.apache.guacamole.net.GuacamoleTunnel; 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.protocol.GuacamoleStatus; import org.slf4j.Logger; @@ -54,6 +57,15 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint { */ 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. */ @@ -61,10 +73,17 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint { /** * 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.Basic remote; + /** * Sends the numeric Guacaomle Status Code and Web Socket * code and closes the connection. @@ -107,6 +126,52 @@ private void closeConnection(Session session, GuacamoleStatus guacStatus) { guacStatus.getWebSocketCode()); } + /** + * 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.sendText(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. @@ -126,6 +191,9 @@ protected abstract GuacamoleTunnel createTunnel(Session session, EndpointConfig @OnOpen public void onOpen(final Session session, EndpointConfig config) { + // Store underlying remote for future use via sendInstruction() + remote = session.getBasicRemote(); + try { // Get tunnel @@ -157,11 +225,6 @@ public void onMessage(String message) { // Prepare read transfer thread Thread readThread = new Thread() { - /** - * Remote (client) side of this connection - */ - private final RemoteEndpoint.Basic remote = session.getBasicRemote(); - @Override public void run() { @@ -172,10 +235,10 @@ public void run() { try { // Send tunnel UUID - remote.sendText(new GuacamoleInstruction( + sendInstruction(new GuacamoleInstruction( GuacamoleTunnel.INTERNAL_DATA_OPCODE, tunnel.getUUID().toString() - ).toString()); + )); try { @@ -187,7 +250,7 @@ public void run() { // Flush if we expect to wait or buffer is getting full if (!reader.available() || buffer.length() >= BUFFER_SIZE) { - remote.sendText(buffer.toString()); + sendInstruction(buffer.toString()); buffer.setLength(0); } @@ -239,7 +302,43 @@ public void onMessage(String message) { 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 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