mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 05:31:22 +00:00
GUACAMOLE-44: Merge front end changes for managing tunnels and handling file transfer.
This commit is contained in:
@@ -70,6 +70,14 @@ Guacamole.Tunnel = function() {
|
|||||||
*/
|
*/
|
||||||
this.receiveTimeout = 15000;
|
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.
|
* 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.
|
* All possible tunnel states.
|
||||||
*/
|
*/
|
||||||
@@ -152,8 +172,6 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) {
|
|||||||
*/
|
*/
|
||||||
var tunnel = this;
|
var tunnel = this;
|
||||||
|
|
||||||
var tunnel_uuid;
|
|
||||||
|
|
||||||
var TUNNEL_CONNECT = tunnelURL + "?connect";
|
var TUNNEL_CONNECT = tunnelURL + "?connect";
|
||||||
var TUNNEL_READ = tunnelURL + "?read:";
|
var TUNNEL_READ = tunnelURL + "?read:";
|
||||||
var TUNNEL_WRITE = tunnelURL + "?write:";
|
var TUNNEL_WRITE = tunnelURL + "?write:";
|
||||||
@@ -286,7 +304,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) {
|
|||||||
sendingMessages = true;
|
sendingMessages = true;
|
||||||
|
|
||||||
var message_xmlhttprequest = new XMLHttpRequest();
|
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.withCredentials = withCredentials;
|
||||||
message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
|
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
|
// Make request, increment request ID
|
||||||
var xmlhttprequest = new XMLHttpRequest();
|
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.withCredentials = withCredentials;
|
||||||
xmlhttprequest.send(null);
|
xmlhttprequest.send(null);
|
||||||
|
|
||||||
@@ -546,7 +564,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) {
|
|||||||
reset_timeout();
|
reset_timeout();
|
||||||
|
|
||||||
// Get UUID from response
|
// Get UUID from response
|
||||||
tunnel_uuid = connect_xmlhttprequest.responseText;
|
tunnel.uuid = connect_xmlhttprequest.responseText;
|
||||||
|
|
||||||
tunnel.state = Guacamole.Tunnel.State.OPEN;
|
tunnel.state = Guacamole.Tunnel.State.OPEN;
|
||||||
if (tunnel.onstatechange)
|
if (tunnel.onstatechange)
|
||||||
@@ -733,13 +751,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
|
|||||||
socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
|
socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
|
||||||
|
|
||||||
socket.onopen = function(event) {
|
socket.onopen = function(event) {
|
||||||
|
|
||||||
reset_timeout();
|
reset_timeout();
|
||||||
|
|
||||||
tunnel.state = Guacamole.Tunnel.State.OPEN;
|
|
||||||
if (tunnel.onstatechange)
|
|
||||||
tunnel.onstatechange(tunnel.state);
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onclose = function(event) {
|
socket.onclose = function(event) {
|
||||||
@@ -794,8 +806,22 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
|
|||||||
// Get opcode
|
// Get opcode
|
||||||
var opcode = elements.shift();
|
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.
|
// Call instruction handler.
|
||||||
if (tunnel.oninstruction)
|
if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction)
|
||||||
tunnel.oninstruction(opcode, elements);
|
tunnel.oninstruction(opcode, elements);
|
||||||
|
|
||||||
// Clear elements
|
// Clear elements
|
||||||
@@ -926,6 +952,7 @@ Guacamole.ChainedTunnel = function(tunnelChain) {
|
|||||||
tunnel.onstatechange = chained_tunnel.onstatechange;
|
tunnel.onstatechange = chained_tunnel.onstatechange;
|
||||||
tunnel.oninstruction = chained_tunnel.oninstruction;
|
tunnel.oninstruction = chained_tunnel.oninstruction;
|
||||||
tunnel.onerror = chained_tunnel.onerror;
|
tunnel.onerror = chained_tunnel.onerror;
|
||||||
|
chained_tunnel.uuid = tunnel.uuid;
|
||||||
committedTunnel = tunnel;
|
committedTunnel = tunnel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -33,6 +33,16 @@ import org.apache.guacamole.io.GuacamoleWriter;
|
|||||||
*/
|
*/
|
||||||
public interface GuacamoleTunnel {
|
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
|
* Acquires exclusive read access to the Guacamole instruction stream
|
||||||
* and returns a GuacamoleReader for reading from that stream.
|
* and returns a GuacamoleReader for reading from that stream.
|
||||||
|
@@ -36,6 +36,7 @@ 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.GuacamoleInstruction;
|
||||||
import org.apache.guacamole.protocol.GuacamoleStatus;
|
import org.apache.guacamole.protocol.GuacamoleStatus;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -149,6 +150,12 @@ public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// Send tunnel UUID
|
||||||
|
remote.sendText(new GuacamoleInstruction(
|
||||||
|
GuacamoleTunnel.INTERNAL_DATA_OPCODE,
|
||||||
|
tunnel.getUUID().toString()
|
||||||
|
).toString());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Attempt to read
|
// Attempt to read
|
||||||
|
@@ -23,11 +23,11 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
|
||||||
import org.apache.guacamole.environment.Environment;
|
import org.apache.guacamole.environment.Environment;
|
||||||
import org.apache.guacamole.net.GuacamoleTunnel;
|
import org.apache.guacamole.net.GuacamoleTunnel;
|
||||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||||
import org.apache.guacamole.net.auth.UserContext;
|
import org.apache.guacamole.net.auth.UserContext;
|
||||||
|
import org.apache.guacamole.tunnel.StreamInterceptingTunnel;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -58,7 +58,8 @@ public class GuacamoleSession {
|
|||||||
/**
|
/**
|
||||||
* All currently-active tunnels, indexed by tunnel UUID.
|
* All currently-active tunnels, indexed by tunnel UUID.
|
||||||
*/
|
*/
|
||||||
private final Map<String, GuacamoleTunnel> tunnels = new ConcurrentHashMap<String, GuacamoleTunnel>();
|
private final Map<String, StreamInterceptingTunnel> tunnels =
|
||||||
|
new ConcurrentHashMap<String, StreamInterceptingTunnel>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last time this session was accessed.
|
* 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.
|
* @return A map of all active tunnels associated with this session.
|
||||||
*/
|
*/
|
||||||
public Map<String, GuacamoleTunnel> getTunnels() {
|
public Map<String, StreamInterceptingTunnel> getTunnels() {
|
||||||
return tunnels;
|
return tunnels;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +167,7 @@ public class GuacamoleSession {
|
|||||||
*
|
*
|
||||||
* @param tunnel The tunnel to associate with this session.
|
* @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);
|
tunnels.put(tunnel.getUUID().toString(), tunnel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -37,6 +37,7 @@ import org.apache.guacamole.rest.history.HistoryRESTService;
|
|||||||
import org.apache.guacamole.rest.language.LanguageRESTService;
|
import org.apache.guacamole.rest.language.LanguageRESTService;
|
||||||
import org.apache.guacamole.rest.patch.PatchRESTService;
|
import org.apache.guacamole.rest.patch.PatchRESTService;
|
||||||
import org.apache.guacamole.rest.schema.SchemaRESTService;
|
import org.apache.guacamole.rest.schema.SchemaRESTService;
|
||||||
|
import org.apache.guacamole.rest.tunnel.TunnelRESTService;
|
||||||
import org.apache.guacamole.rest.user.UserRESTService;
|
import org.apache.guacamole.rest.user.UserRESTService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,6 +93,7 @@ public class RESTServiceModule extends ServletModule {
|
|||||||
bind(PatchRESTService.class);
|
bind(PatchRESTService.class);
|
||||||
bind(SchemaRESTService.class);
|
bind(SchemaRESTService.class);
|
||||||
bind(TokenRESTService.class);
|
bind(TokenRESTService.class);
|
||||||
|
bind(TunnelRESTService.class);
|
||||||
bind(UserRESTService.class);
|
bind(UserRESTService.class);
|
||||||
|
|
||||||
// Set up the servlet and JSON mappings
|
// Set up the servlet and JSON mappings
|
||||||
|
@@ -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<String> 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<String, StreamInterceptingTunnel> 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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<String, OutputStream> streams =
|
||||||
|
new ConcurrentHashMap<String, OutputStream>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<String> 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<String> 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -23,13 +23,9 @@ import com.google.inject.Inject;
|
|||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
|
||||||
import org.apache.guacamole.GuacamoleSecurityException;
|
|
||||||
import org.apache.guacamole.GuacamoleSecurityException;
|
import org.apache.guacamole.GuacamoleSecurityException;
|
||||||
import org.apache.guacamole.GuacamoleSession;
|
import org.apache.guacamole.GuacamoleSession;
|
||||||
import org.apache.guacamole.GuacamoleUnauthorizedException;
|
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.GuacamoleTunnel;
|
||||||
import org.apache.guacamole.net.auth.Connection;
|
import org.apache.guacamole.net.auth.Connection;
|
||||||
import org.apache.guacamole.net.auth.ConnectionGroup;
|
import org.apache.guacamole.net.auth.ConnectionGroup;
|
||||||
@@ -242,7 +238,7 @@ public class TunnelRequestService {
|
|||||||
throws GuacamoleException {
|
throws GuacamoleException {
|
||||||
|
|
||||||
// Monitor tunnel closure and data
|
// Monitor tunnel closure and data
|
||||||
GuacamoleTunnel monitoredTunnel = new DelegatingGuacamoleTunnel(tunnel) {
|
StreamInterceptingTunnel monitoredTunnel = new StreamInterceptingTunnel(tunnel) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time the connection began, measured in milliseconds since
|
* The time the connection began, measured in milliseconds since
|
||||||
|
@@ -30,6 +30,7 @@ import org.eclipse.jetty.websocket.WebSocket.Connection;
|
|||||||
import org.eclipse.jetty.websocket.WebSocketServlet;
|
import org.eclipse.jetty.websocket.WebSocketServlet;
|
||||||
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.GuacamoleInstruction;
|
||||||
import org.apache.guacamole.tunnel.http.HTTPTunnelRequest;
|
import org.apache.guacamole.tunnel.http.HTTPTunnelRequest;
|
||||||
import org.apache.guacamole.tunnel.TunnelRequest;
|
import org.apache.guacamole.tunnel.TunnelRequest;
|
||||||
import org.apache.guacamole.protocol.GuacamoleStatus;
|
import org.apache.guacamole.protocol.GuacamoleStatus;
|
||||||
@@ -136,6 +137,12 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// Send tunnel UUID
|
||||||
|
connection.sendMessage(new GuacamoleInstruction(
|
||||||
|
GuacamoleTunnel.INTERNAL_DATA_OPCODE,
|
||||||
|
tunnel.getUUID().toString()
|
||||||
|
).toString());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Attempt to read
|
// Attempt to read
|
||||||
|
@@ -30,6 +30,7 @@ import org.apache.guacamole.GuacamoleException;
|
|||||||
import org.apache.guacamole.io.GuacamoleReader;
|
import org.apache.guacamole.io.GuacamoleReader;
|
||||||
import org.apache.guacamole.io.GuacamoleWriter;
|
import org.apache.guacamole.io.GuacamoleWriter;
|
||||||
import org.apache.guacamole.net.GuacamoleTunnel;
|
import org.apache.guacamole.net.GuacamoleTunnel;
|
||||||
|
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;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -127,6 +128,12 @@ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListe
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// Send tunnel UUID
|
||||||
|
remote.sendString(new GuacamoleInstruction(
|
||||||
|
GuacamoleTunnel.INTERNAL_DATA_OPCODE,
|
||||||
|
tunnel.getUUID().toString()
|
||||||
|
).toString());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Attempt to read
|
// Attempt to read
|
||||||
|
@@ -35,6 +35,7 @@ import org.apache.catalina.websocket.WebSocketServlet;
|
|||||||
import org.apache.catalina.websocket.WsOutbound;
|
import org.apache.catalina.websocket.WsOutbound;
|
||||||
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.GuacamoleInstruction;
|
||||||
import org.apache.guacamole.tunnel.http.HTTPTunnelRequest;
|
import org.apache.guacamole.tunnel.http.HTTPTunnelRequest;
|
||||||
import org.apache.guacamole.tunnel.TunnelRequest;
|
import org.apache.guacamole.tunnel.TunnelRequest;
|
||||||
import org.apache.guacamole.protocol.GuacamoleStatus;
|
import org.apache.guacamole.protocol.GuacamoleStatus;
|
||||||
@@ -164,6 +165,12 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// Send tunnel UUID
|
||||||
|
outbound.writeTextMessage(CharBuffer.wrap(new GuacamoleInstruction(
|
||||||
|
GuacamoleTunnel.INTERNAL_DATA_OPCODE,
|
||||||
|
tunnel.getUUID().toString()
|
||||||
|
).toString()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Attempt to read
|
// Attempt to read
|
||||||
|
127
guacamole/src/main/webapp/app/rest/services/tunnelService.js
Normal file
127
guacamole/src/main/webapp/app/rest/services/tunnelService.js
Normal file
@@ -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.<String[]>>}
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
}]);
|
Reference in New Issue
Block a user