Merge 1.0.0 changes back to master.

This commit is contained in:
Nick Couchman
2018-09-07 19:06:20 -04:00
8 changed files with 543 additions and 52 deletions

View File

@@ -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;
@@ -52,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";
/**
* Sends the given numeric Guacamole and WebSocket status
* on the given WebSocket connection and closes the
@@ -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);
}

View File

@@ -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;
@@ -45,6 +48,15 @@ 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.
*/
@@ -52,10 +64,17 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe
/**
* 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
@@ -101,6 +120,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

View File

@@ -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;
@@ -52,6 +54,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.
*/
@@ -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);
}

View File

@@ -635,7 +635,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
* otherwise.
*/
$scope.isConnectionUnstable = function isConnectionUnstable() {
return $scope.client && $scope.client.clientState.connectionState === ManagedClientState.ConnectionState.UNSTABLE;
return $scope.client && $scope.client.clientState.tunnelUnstable;
};
// Show status dialog when connection status changes

View File

@@ -346,16 +346,14 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
ManagedClientState.ConnectionState.CONNECTING);
break;
// Connection is established
// Connection is established / no longer unstable
case Guacamole.Tunnel.State.OPEN:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTED);
ManagedClientState.setTunnelUnstable(managedClient.clientState, false);
break;
// Connection is established but misbehaving
case Guacamole.Tunnel.State.UNSTABLE:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.UNSTABLE);
ManagedClientState.setTunnelUnstable(managedClient.clientState, true);
break;
// Connection has closed

View File

@@ -45,6 +45,16 @@ angular.module('client').factory('ManagedClientState', [function defineManagedCl
*/
this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE;
/**
* Whether the network connection used by the tunnel seems unstable. If
* the network connection is unstable, the remote desktop connection
* may perform poorly or disconnect.
*
* @type Boolean
* @default false
*/
this.tunnelUnstable = template.tunnelUnstable || false;
/**
* The status code of the current error condition, if connectionState
* is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState
@@ -93,15 +103,6 @@ angular.module('client').factory('ManagedClientState', [function defineManagedCl
*/
CONNECTED : "CONNECTED",
/**
* The Guacamole connection has been successfully established, but the
* network connection seems unstable. The connection may perform poorly
* or disconnect.
*
* @type String
*/
UNSTABLE : "UNSTABLE",
/**
* The Guacamole connection has terminated successfully. No errors are
* indicated.
@@ -130,7 +131,9 @@ angular.module('client').factory('ManagedClientState', [function defineManagedCl
/**
* Sets the current client state and, if given, the associated status code.
* If an error is already represented, this function has no effect.
* If an error is already represented, this function has no effect. If the
* client state was previously marked as unstable, that flag is implicitly
* cleared.
*
* @param {ManagedClientState} clientState
* The ManagedClientState to update.
@@ -153,6 +156,7 @@ angular.module('client').factory('ManagedClientState', [function defineManagedCl
// Update connection state
clientState.connectionState = connectionState;
clientState.tunnelUnstable = false;
// Set status code, if given
if (statusCode)
@@ -160,6 +164,22 @@ angular.module('client').factory('ManagedClientState', [function defineManagedCl
};
/**
* Updates the given client state, setting whether the underlying tunnel
* is currently unstable. An unstable tunnel is not necessarily
* disconnected, but appears to be misbehaving and may be disconnected.
*
* @param {ManagedClientState} clientState
* The ManagedClientState to update.
*
* @param {Boolean} unstable
* Whether the underlying tunnel of the connection currently appears
* unstable.
*/
ManagedClientState.setTunnelUnstable = function setTunnelUnstable(clientState, unstable) {
clientState.tunnelUnstable = unstable;
};
return ManagedClientState;
}]);