GUACAMOLE-567: Use ping messages specific to the WebSocket tunnel to test connection stability independently of the underlying Guacamole connection.

This commit is contained in:
Michael Jumper
2018-09-06 19:48:33 -07:00
parent 5825835237
commit ea0b33bee1
2 changed files with 141 additions and 12 deletions

View File

@@ -153,7 +153,8 @@ Guacamole.Tunnel = function() {
* use by tunnel implementations. The value of this opcode is guaranteed to be * use by tunnel implementations. The value of this opcode is guaranteed to be
* the empty string (""). Tunnel implementations may use this opcode for any * 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 * 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 * @constant
* @type {String} * @type {String}
@@ -742,6 +743,15 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
*/ */
var unstableTimeout = null; 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 * The WebSocket protocol corresponding to the protocol used for the current
* location. * location.
@@ -752,6 +762,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
"https:": "wss:" "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 // Transform current URL to WebSocket URL
// If not already a websocket URL // If not already a websocket URL
@@ -828,6 +848,9 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
window.clearTimeout(receive_timeout); window.clearTimeout(receive_timeout);
window.clearTimeout(unstableTimeout); window.clearTimeout(unstableTimeout);
// Cease connection test pings
window.clearInterval(pingInterval);
// Ignore if already closed // Ignore if already closed
if (tunnel.state === Guacamole.Tunnel.State.CLOSED) if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
return; return;
@@ -892,6 +915,13 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
socket.onopen = function(event) { socket.onopen = function(event) {
reset_timeout(); 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) { socket.onclose = function(event) {

View File

@@ -20,6 +20,7 @@
package org.apache.guacamole.websocket; package org.apache.guacamole.websocket;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import javax.websocket.CloseReason; import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCode; import javax.websocket.CloseReason.CloseCode;
import javax.websocket.Endpoint; import javax.websocket.Endpoint;
@@ -36,6 +37,8 @@ import org.apache.guacamole.io.GuacamoleWriter;
import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleConnectionClosedException; 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.GuacamoleInstruction;
import org.apache.guacamole.protocol.GuacamoleStatus; import org.apache.guacamole.protocol.GuacamoleStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -54,6 +57,15 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
*/ */
private static final int BUFFER_SIZE = 8192; 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. * Logger for this class.
*/ */
@@ -61,10 +73,17 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
/** /**
* The underlying GuacamoleTunnel. WebSocket reads/writes will be handled * 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; 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 * Sends the numeric Guacaomle Status Code and Web Socket
* code and closes the connection. * code and closes the connection.
@@ -107,6 +126,52 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
guacStatus.getWebSocketCode()); 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 * Returns a new tunnel for the given session. How this tunnel is created
* or retrieved is implementation-dependent. * or retrieved is implementation-dependent.
@@ -126,6 +191,9 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
@OnOpen @OnOpen
public void onOpen(final Session session, EndpointConfig config) { public void onOpen(final Session session, EndpointConfig config) {
// Store underlying remote for future use via sendInstruction()
remote = session.getBasicRemote();
try { try {
// Get tunnel // Get tunnel
@@ -157,11 +225,6 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
// Prepare read transfer thread // Prepare read transfer thread
Thread readThread = new Thread() { Thread readThread = new Thread() {
/**
* Remote (client) side of this connection
*/
private final RemoteEndpoint.Basic remote = session.getBasicRemote();
@Override @Override
public void run() { public void run() {
@@ -172,10 +235,10 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
try { try {
// Send tunnel UUID // Send tunnel UUID
remote.sendText(new GuacamoleInstruction( sendInstruction(new GuacamoleInstruction(
GuacamoleTunnel.INTERNAL_DATA_OPCODE, GuacamoleTunnel.INTERNAL_DATA_OPCODE,
tunnel.getUUID().toString() tunnel.getUUID().toString()
).toString()); ));
try { try {
@@ -187,7 +250,7 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
// Flush if we expect to wait or buffer is getting full // Flush if we expect to wait or buffer is getting full
if (!reader.available() || buffer.length() >= BUFFER_SIZE) { if (!reader.available() || buffer.length() >= BUFFER_SIZE) {
remote.sendText(buffer.toString()); sendInstruction(buffer.toString());
buffer.setLength(0); buffer.setLength(0);
} }
@@ -239,7 +302,43 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
if (tunnel == null) if (tunnel == null)
return; 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 { try {
// Write received message // Write received message