diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index 3fcb4099e..6ec479821 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -70,6 +70,14 @@ Guacamole.Tunnel = function() { */ this.receiveTimeout = 15000; + /** + * The UUID uniquely identifying this tunnel. If not yet known, this will + * be null. + * + * @type {String} + */ + this.uuid = null; + /** * Fired whenever an error is encountered by the tunnel. * @@ -99,6 +107,18 @@ Guacamole.Tunnel = function() { }; +/** + * The Guacamole protocol instruction opcode reserved for arbitrary internal + * 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. + * + * @constant + * @type {String} + */ +Guacamole.Tunnel.INTERNAL_DATA_OPCODE = ''; + /** * All possible tunnel states. */ @@ -152,8 +172,6 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) { */ var tunnel = this; - var tunnel_uuid; - var TUNNEL_CONNECT = tunnelURL + "?connect"; var TUNNEL_READ = tunnelURL + "?read:"; var TUNNEL_WRITE = tunnelURL + "?write:"; @@ -286,7 +304,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) { sendingMessages = true; var message_xmlhttprequest = new XMLHttpRequest(); - message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); + message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid); message_xmlhttprequest.withCredentials = withCredentials; message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); @@ -517,7 +535,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) { // Make request, increment request ID var xmlhttprequest = new XMLHttpRequest(); - xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++)); + xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++)); xmlhttprequest.withCredentials = withCredentials; xmlhttprequest.send(null); @@ -546,7 +564,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) { reset_timeout(); // Get UUID from response - tunnel_uuid = connect_xmlhttprequest.responseText; + tunnel.uuid = connect_xmlhttprequest.responseText; tunnel.state = Guacamole.Tunnel.State.OPEN; if (tunnel.onstatechange) @@ -733,13 +751,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); socket.onopen = function(event) { - reset_timeout(); - - tunnel.state = Guacamole.Tunnel.State.OPEN; - if (tunnel.onstatechange) - tunnel.onstatechange(tunnel.state); - }; socket.onclose = function(event) { @@ -794,8 +806,22 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { // Get opcode var opcode = elements.shift(); + // Update state and UUID when first instruction received + if (tunnel.state !== Guacamole.Tunnel.State.OPEN) { + + // Associate tunnel UUID if received + if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE) + tunnel.uuid = elements[0]; + + // Tunnel is now open and UUID is available + tunnel.state = Guacamole.Tunnel.State.OPEN; + if (tunnel.onstatechange) + tunnel.onstatechange(tunnel.state); + + } + // Call instruction handler. - if (tunnel.oninstruction) + if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction) tunnel.oninstruction(opcode, elements); // Clear elements @@ -926,6 +952,7 @@ Guacamole.ChainedTunnel = function(tunnelChain) { tunnel.onstatechange = chained_tunnel.onstatechange; tunnel.oninstruction = chained_tunnel.oninstruction; tunnel.onerror = chained_tunnel.onerror; + chained_tunnel.uuid = tunnel.uuid; committedTunnel = tunnel; } diff --git a/guacamole-common/src/main/java/org/apache/guacamole/net/GuacamoleTunnel.java b/guacamole-common/src/main/java/org/apache/guacamole/net/GuacamoleTunnel.java index b53c474c8..77c00e1f2 100644 --- a/guacamole-common/src/main/java/org/apache/guacamole/net/GuacamoleTunnel.java +++ b/guacamole-common/src/main/java/org/apache/guacamole/net/GuacamoleTunnel.java @@ -33,6 +33,16 @@ import org.apache.guacamole.io.GuacamoleWriter; */ public interface GuacamoleTunnel { + /** + * The Guacamole protocol instruction opcode reserved for arbitrary + * internal 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. + */ + static final String INTERNAL_DATA_OPCODE = ""; + /** * Acquires exclusive read access to the Guacamole instruction stream * and returns a GuacamoleReader for reading from that stream. 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 dbded9b8f..e0aa44224 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 @@ -36,6 +36,7 @@ import org.apache.guacamole.io.GuacamoleWriter; import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleConnectionClosedException; +import org.apache.guacamole.protocol.GuacamoleInstruction; import org.apache.guacamole.protocol.GuacamoleStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -149,6 +150,12 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint { try { + // Send tunnel UUID + remote.sendText(new GuacamoleInstruction( + GuacamoleTunnel.INTERNAL_DATA_OPCODE, + tunnel.getUUID().toString() + ).toString()); + try { // Attempt to read diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java index 601b2917b..06530c2f1 100644 --- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java @@ -23,11 +23,11 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.tunnel.StreamInterceptingTunnel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +58,8 @@ public class GuacamoleSession { /** * All currently-active tunnels, indexed by tunnel UUID. */ - private final Map tunnels = new ConcurrentHashMap(); + private final Map tunnels = + new ConcurrentHashMap(); /** * The last time this session was accessed. @@ -156,7 +157,7 @@ public class GuacamoleSession { * * @return A map of all active tunnels associated with this session. */ - public Map getTunnels() { + public Map getTunnels() { return tunnels; } @@ -166,7 +167,7 @@ public class GuacamoleSession { * * @param tunnel The tunnel to associate with this session. */ - public void addTunnel(GuacamoleTunnel tunnel) { + public void addTunnel(StreamInterceptingTunnel tunnel) { tunnels.put(tunnel.getUUID().toString(), tunnel); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java index 45ee0614b..ec555e30e 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java @@ -37,6 +37,7 @@ import org.apache.guacamole.rest.history.HistoryRESTService; import org.apache.guacamole.rest.language.LanguageRESTService; import org.apache.guacamole.rest.patch.PatchRESTService; import org.apache.guacamole.rest.schema.SchemaRESTService; +import org.apache.guacamole.rest.tunnel.TunnelRESTService; import org.apache.guacamole.rest.user.UserRESTService; /** @@ -92,6 +93,7 @@ public class RESTServiceModule extends ServletModule { bind(PatchRESTService.class); bind(SchemaRESTService.class); bind(TokenRESTService.class); + bind(TunnelRESTService.class); bind(UserRESTService.class); // Set up the servlet and JSON mappings diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelRESTService.java b/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelRESTService.java new file mode 100644 index 000000000..c89fdcd25 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelRESTService.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.rest.tunnel; + +import com.google.inject.Inject; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; +import java.util.Set; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.GuacamoleSession; +import org.apache.guacamole.rest.auth.AuthenticationService; +import org.apache.guacamole.tunnel.StreamInterceptingTunnel; + +/** + * A REST Service for retrieving and managing the tunnels of active + * connections, including any associated objects. + * + * @author Michael Jumper + */ +@Path("/tunnels") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class TunnelRESTService { + + /** + * The media type to send as the content type of stream contents if no + * other media type is specified. + */ + private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream"; + + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; + + /** + * Returns the UUIDs of all currently-active tunnels associated with the + * session identified by the given auth token. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @return + * A set containing the UUIDs of all currently-active tunnels. + * + * @throws GuacamoleException + * If the session associated with the given auth token cannot be + * retrieved. + */ + @GET + public Set getTunnelUUIDs(@QueryParam("token") String authToken) + throws GuacamoleException { + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + return session.getTunnels().keySet(); + } + + /** + * Intercepts and returns the entire contents of a specific stream. + * + * @param authToken + * The authentication token that is used to authenticate the user + * performing the operation. + * + * @param tunnelUUID + * The UUID of the tunnel containing the stream being intercepted. + * + * @param streamIndex + * The index of the stream to intercept. + * + * @param mediaType + * The media type (mimetype) of the data within the stream. + * + * @param filename + * The filename to use for the sake of identifying the data returned. + * + * @return + * A response through which the entire contents of the intercepted + * stream will be sent. + * + * @throws GuacamoleException + * If the session associated with the given auth token cannot be + * retrieved, or if no such tunnel exists. + */ + @GET + @Path("/{tunnel}/streams/{index}/{filename}") + public Response getStreamContents(@QueryParam("token") String authToken, + @PathParam("tunnel") String tunnelUUID, + @PathParam("index") final int streamIndex, + @QueryParam("type") @DefaultValue(DEFAULT_MEDIA_TYPE) String mediaType, + @PathParam("filename") String filename) + throws GuacamoleException { + + GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + Map tunnels = session.getTunnels(); + + // Pull tunnel with given UUID + final StreamInterceptingTunnel tunnel = tunnels.get(tunnelUUID); + if (tunnel == null) + throw new GuacamoleResourceNotFoundException("No such tunnel."); + + // Intercept all output + StreamingOutput stream = new StreamingOutput() { + + @Override + public void write(OutputStream output) throws IOException { + tunnel.interceptStream(streamIndex, output); + } + + }; + + return Response.ok(stream, mediaType).build(); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/StreamInterceptingTunnel.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/StreamInterceptingTunnel.java new file mode 100644 index 000000000..a9a135512 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/StreamInterceptingTunnel.java @@ -0,0 +1,369 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.tunnel; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.xml.bind.DatatypeConverter; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.io.GuacamoleReader; +import org.apache.guacamole.io.GuacamoleWriter; +import org.apache.guacamole.net.DelegatingGuacamoleTunnel; +import org.apache.guacamole.net.GuacamoleTunnel; +import org.apache.guacamole.protocol.FilteredGuacamoleReader; +import org.apache.guacamole.protocol.GuacamoleFilter; +import org.apache.guacamole.protocol.GuacamoleInstruction; +import org.apache.guacamole.protocol.GuacamoleStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GuacamoleTunnel implementation which provides for intercepting the contents + * of in-progress streams, rerouting received blobs to a provided OutputStream. + * Interception of streams is requested on a per stream basis and lasts only + * for the duration of that stream. + * + * @author Michael Jumper + */ +public class StreamInterceptingTunnel extends DelegatingGuacamoleTunnel { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(StreamInterceptingTunnel.class); + + /** + * The maximum number of milliseconds to wait for notification that a + * stream has closed before explicitly checking for closure ourselves. + */ + private static final long STREAM_WAIT_TIMEOUT = 1000; + + /** + * Creates a new StreamInterceptingTunnel which wraps the given tunnel, + * reading and intercepting stream-related instructions as necessary to + * fulfill calls to interceptStream(). + * + * @param tunnel + * The tunnel whose stream-related instruction should be intercepted if + * interceptStream() is invoked. + */ + public StreamInterceptingTunnel(GuacamoleTunnel tunnel) { + super(tunnel); + } + + /** + * Mapping of the indexes of all streams whose associated "blob" and "end" + * instructions should be intercepted. + */ + private final Map streams = + new ConcurrentHashMap(); + + /** + * Filter which selectively intercepts "blob" and "end" instructions, + * automatically writing to or closing the stream given with + * interceptStream(). The required "ack" responses to received blobs are + * sent automatically. + */ + private final GuacamoleFilter STREAM_FILTER = new GuacamoleFilter() { + + /** + * Handles a single "blob" instruction, decoding its base64 data, + * sending that data to the associated OutputStream, and ultimately + * dropping the "blob" instruction such that the client never receives + * it. If no OutputStream is associated with the stream index within + * the "blob" instruction, the instruction is passed through untouched. + * + * @param instruction + * The "blob" instruction being handled. + * + * @return + * The originally-provided "blob" instruction, if that instruction + * should be passed through to the client, or null if the "blob" + * instruction should be dropped. + */ + private GuacamoleInstruction handleBlob(GuacamoleInstruction instruction) { + + // Verify all required arguments are present + List args = instruction.getArgs(); + if (args.size() < 2) + return instruction; + + // Pull associated stream + String index = args.get(0); + OutputStream stream = streams.get(index); + if (stream == null) + return instruction; + + // Decode blob + byte[] blob; + try { + String data = args.get(1); + blob = DatatypeConverter.parseBase64Binary(data); + } + catch (IllegalArgumentException e) { + logger.warn("Received base64 data for intercepted stream was invalid."); + logger.debug("Decoding base64 data for intercepted stream failed.", e); + return null; + } + + // Attempt to write data to stream + try { + stream.write(blob); + sendAck(index, "OK", GuacamoleStatus.SUCCESS); + } + catch (IOException e) { + sendAck(index, "FAIL", GuacamoleStatus.SERVER_ERROR); + logger.debug("Write failed for intercepted stream.", e); + } + + // Instruction was handled purely internally + return null; + + } + + /** + * Handles a single "end" instruction, closing the associated + * OutputStream. If no OutputStream is associated with the stream index + * within the "end" instruction, this function has no effect. + * + * @param instruction + * The "end" instruction being handled. + */ + private void handleEnd(GuacamoleInstruction instruction) { + + // Verify all required arguments are present + List args = instruction.getArgs(); + if (args.size() < 1) + return; + + // Terminate stream + closeStream(args.get(0)); + + } + + @Override + public GuacamoleInstruction filter(GuacamoleInstruction instruction) + throws GuacamoleException { + + // Intercept "blob" instructions for in-progress streams + if (instruction.getOpcode().equals("blob")) + return handleBlob(instruction); + + // Intercept "end" instructions for in-progress streams + if (instruction.getOpcode().equals("end")) { + handleEnd(instruction); + return instruction; + } + + // Pass instruction through untouched + return instruction; + + } + + }; + + /** + * Closes the given OutputStream, logging any errors that occur during + * closure. The monitor of the OutputStream is notified via a single call + * to notify() once the attempt to close has been made. + * + * @param stream + * The OutputStream to close and notify. + */ + private void closeStream(OutputStream stream) { + + // Attempt to close stream + try { + stream.close(); + } + catch (IOException e) { + logger.warn("Unable to close intercepted stream: {}", e.getMessage()); + logger.debug("I/O error prevented closure of intercepted stream.", e); + } + + // Notify waiting threads that the stream has ended + synchronized (stream) { + stream.notify(); + } + + } + + /** + * Closes the OutputStream associated with the stream having the given + * index, if any, logging any errors that occur during closure. If no such + * stream exists, this function has no effect. The monitor of the + * OutputStream is notified via a single call to notify() once the attempt + * to close has been made. + * + * @param index + * The index of the stream whose associated OutputStream should be + * closed and notified. + */ + private OutputStream closeStream(String index) { + + // Remove associated stream + OutputStream stream = streams.remove(index); + if (stream == null) + return null; + + // Close stream if it exists + closeStream(stream); + return stream; + + } + + /** + * Injects an "ack" instruction into the outbound Guacamole protocol + * stream, as if sent by the connected client. "ack" instructions are used + * to acknowledge the receipt of a stream and its subsequent blobs, and are + * the only means of communicating success/failure status. + * + * @param index + * The index of the stream that this "ack" instruction relates to. + * + * @param message + * An arbitrary human-readable message to include within the "ack" + * instruction. + * + * @param status + * The status of the stream operation being acknowledged via the "ack" + * instruction. Error statuses will implicitly close the stream via + * closeStream(). + */ + private void sendAck(String index, String message, GuacamoleStatus status) { + + // Temporarily acquire writer to send "ack" instruction + GuacamoleWriter writer = acquireWriter(); + + // Send successful "ack" + try { + writer.writeInstruction(new GuacamoleInstruction("ack", index, message, + Integer.toString(status.getGuacamoleStatusCode()))); + } + catch (GuacamoleException e) { + logger.debug("Unable to send \"ack\" for intercepted stream.", e); + } + + // Error "ack" instructions implicitly close the stream + if (status != GuacamoleStatus.SUCCESS) + closeStream(index); + + // Done writing + releaseWriter(); + + } + + /** + * Intercept all data received along the stream having the given index, + * writing that data to the given OutputStream. The OutputStream will + * automatically be closed when the stream ends. If there is no such + * stream, then the OutputStream will be closed immediately. This function + * will block until all received data has been written to the OutputStream + * and the OutputStream has been closed. + * + * @param index + * The index of the stream to intercept. + * + * @param stream + * The OutputStream to write all intercepted data to. + */ + public void interceptStream(int index, OutputStream stream) { + + String indexString = Integer.toString(index); + + // Atomically verify tunnel is open and add the given stream + OutputStream oldStream; + synchronized (this) { + + // Do nothing if tunnel is not open + if (!isOpen()) { + closeStream(stream); + return; + } + + // Wrap stream + stream = new BufferedOutputStream(stream); + + // Replace any existing stream + oldStream = streams.put(indexString, stream); + + } + + // If a previous stream DID exist, close it + if (oldStream != null) + closeStream(oldStream); + + // Log beginning of intercepted stream + logger.debug("Intercepting stream #{} of tunnel \"{}\".", + index, getUUID()); + + // Acknowledge stream receipt + sendAck(indexString, "OK", GuacamoleStatus.SUCCESS); + + // Wait for stream to close + synchronized (stream) { + while (streams.get(indexString) == stream) { + try { + stream.wait(STREAM_WAIT_TIMEOUT); + } + catch (InterruptedException e) { + // Ignore + } + } + } + + // Log end of intercepted stream + logger.debug("Intercepted stream #{} of tunnel \"{}\" ended.", index, getUUID()); + + } + + @Override + public GuacamoleReader acquireReader() { + return new FilteredGuacamoleReader(super.acquireReader(), STREAM_FILTER); + } + + @Override + public synchronized void close() throws GuacamoleException { + + // Close first, such that no further streams can be added via + // interceptStream() + try { + super.close(); + } + + // Ensure all waiting threads are notified that all streams have ended + finally { + + // Close any active streams + for (OutputStream stream : streams.values()) + closeStream(stream); + + // Remove now-useless references + streams.clear(); + + } + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java index 3e35dc4b7..80404723a 100644 --- a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java +++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java @@ -23,13 +23,9 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.List; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleSession; import org.apache.guacamole.GuacamoleUnauthorizedException; -import org.apache.guacamole.GuacamoleUnauthorizedException; -import org.apache.guacamole.net.DelegatingGuacamoleTunnel; import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; @@ -242,7 +238,7 @@ public class TunnelRequestService { throws GuacamoleException { // Monitor tunnel closure and data - GuacamoleTunnel monitoredTunnel = new DelegatingGuacamoleTunnel(tunnel) { + StreamInterceptingTunnel monitoredTunnel = new StreamInterceptingTunnel(tunnel) { /** * The time the connection began, measured in milliseconds since 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 2e02e3483..933ff654f 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 @@ -30,6 +30,7 @@ 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.GuacamoleInstruction; import org.apache.guacamole.tunnel.http.HTTPTunnelRequest; import org.apache.guacamole.tunnel.TunnelRequest; import org.apache.guacamole.protocol.GuacamoleStatus; @@ -136,6 +137,12 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { try { + // Send tunnel UUID + connection.sendMessage(new GuacamoleInstruction( + GuacamoleTunnel.INTERNAL_DATA_OPCODE, + tunnel.getUUID().toString() + ).toString()); + try { // Attempt to read 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 ce5be60ca..89105fc99 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 @@ -30,6 +30,7 @@ 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.GuacamoleInstruction; import org.apache.guacamole.protocol.GuacamoleStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -127,6 +128,12 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe try { + // Send tunnel UUID + remote.sendString(new GuacamoleInstruction( + GuacamoleTunnel.INTERNAL_DATA_OPCODE, + tunnel.getUUID().toString() + ).toString()); + try { // Attempt to read 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 059a0bd7d..1b9098f9b 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,7 @@ 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.GuacamoleInstruction; import org.apache.guacamole.tunnel.http.HTTPTunnelRequest; import org.apache.guacamole.tunnel.TunnelRequest; import org.apache.guacamole.protocol.GuacamoleStatus; @@ -164,6 +165,12 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { try { + // Send tunnel UUID + outbound.writeTextMessage(CharBuffer.wrap(new GuacamoleInstruction( + GuacamoleTunnel.INTERNAL_DATA_OPCODE, + tunnel.getUUID().toString() + ).toString())); + try { // Attempt to read diff --git a/guacamole/src/main/webapp/app/rest/services/tunnelService.js b/guacamole/src/main/webapp/app/rest/services/tunnelService.js new file mode 100644 index 000000000..c97d36154 --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/services/tunnelService.js @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Service for operating on the tunnels of in-progress connections (and their + * underlying objects) via the REST API. + */ +angular.module('rest').factory('tunnelService', ['$injector', + function tunnelService($injector) { + + // Required services + var $http = $injector.get('$http'); + var $window = $injector.get('$window'); + var authenticationService = $injector.get('authenticationService'); + + var service = {}; + + /** + * Reference to the window.document object. + * + * @private + * @type HTMLDocument + */ + var document = $window.document; + + /** + * Makes a request to the REST API to get the list of all tunnels + * associated with in-progress connections, returning a promise that + * provides an array of their UUIDs (strings) if successful. + * + * @returns {Promise.>} + * A promise which will resolve with an array of UUID strings, uniquely + * identifying each active tunnel. + */ + service.getTunnels = function getTunnels() { + + // Build HTTP parameters set + var httpParameters = { + token : authenticationService.getCurrentToken() + }; + + // Retrieve tunnels + return $http({ + method : 'GET', + url : 'api/tunnels', + params : httpParameters + }); + + }; + + /** + * Makes a request to the REST API to retrieve the contents of a stream + * which has been created within the active Guacamole connection associated + * with the given tunnel. The contents of the stream will automatically be + * downloaded by the browser. + * + * WARNING: Like Guacamole's various reader implementations, this function + * relies on assigning an "onend" handler to the stream object for the sake + * of cleaning up resources after the stream closes. If the "onend" handler + * is overwritten after this function returns, resources may not be + * properly cleaned up. + * + * @param {String} tunnel + * The UUID of the tunnel associated with the Guacamole connection + * whose stream should be downloaded as a file. + * + * @param {Guacamole.InputStream} stream + * The stream whose contents should be downloaded. + * + * @param {String} mimetype + * The mimetype of the stream being downloaded. This is currently + * ignored, with the download forced by using + * "application/octet-stream". + * + * @param {String} filename + * The filename that should be given to the downloaded file. + */ + service.downloadStream = function downloadStream(tunnel, stream, mimetype, filename) { + + // Build download URL + var url = $window.location.origin + + $window.location.pathname + + 'api/tunnels/' + encodeURIComponent(tunnel) + + '/streams/' + encodeURIComponent(stream.index) + + '/' + encodeURIComponent(filename) + + '?token=' + encodeURIComponent(authenticationService.getCurrentToken()); + + // Create temporary hidden iframe to facilitate download + var iframe = document.createElement('iframe'); + iframe.style.position = 'fixed'; + iframe.style.width = '1px'; + iframe.style.height = '1px'; + iframe.style.left = '-1px'; + iframe.style.top = '-1px'; + + // The iframe MUST be part of the DOM for the download to occur + document.body.appendChild(iframe); + + // Automatically remove iframe from DOM when download completes + stream.onend = function downloadComplete() { + document.body.removeChild(iframe); + }; + + // Begin download + iframe.src = url; + + }; + + return service; + +}]);