GUACAMOLE-956: Decouple tunnel UUID from HTTP tunnel session identification.

This commit is contained in:
Michael Jumper
2021-10-22 18:28:59 -07:00
parent 1a0802f4a3
commit 0597358dde
3 changed files with 189 additions and 90 deletions

View File

@@ -309,6 +309,25 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
*/ */
var extraHeaders = extraTunnelHeaders || {}; var extraHeaders = extraTunnelHeaders || {};
/**
* The name of the HTTP header containing the session token specific to the
* HTTP tunnel implementation.
*
* @private
* @constant
* @type {string}
*/
var TUNNEL_TOKEN_HEADER = 'Guacamole-Tunnel-Token';
/**
* The session token currently assigned to this HTTP tunnel. All distinct
* HTTP tunnel connections will have their own dedicated session token.
*
* @private
* @type {string}
*/
var tunnelSessionToken = null;
/** /**
* Adds the configured additional headers to the given request. * Adds the configured additional headers to the given request.
* *
@@ -453,6 +472,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
message_xmlhttprequest.withCredentials = withCredentials; message_xmlhttprequest.withCredentials = withCredentials;
addExtraHeaders(message_xmlhttprequest, extraHeaders); addExtraHeaders(message_xmlhttprequest, extraHeaders);
message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream"); message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream");
message_xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
// Once response received, send next queued event. // Once response received, send next queued event.
message_xmlhttprequest.onreadystatechange = function() { message_xmlhttprequest.onreadystatechange = function() {
@@ -697,6 +717,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
// 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.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
xmlhttprequest.withCredentials = withCredentials; xmlhttprequest.withCredentials = withCredentials;
addExtraHeaders(xmlhttprequest, extraHeaders); addExtraHeaders(xmlhttprequest, extraHeaders);
xmlhttprequest.send(null); xmlhttprequest.send(null);
@@ -728,8 +749,15 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
reset_timeout(); reset_timeout();
// Get UUID from response // Get UUID and HTTP-specific tunnel session token from response
tunnel.setUUID(connect_xmlhttprequest.responseText); tunnel.setUUID(connect_xmlhttprequest.responseText);
tunnelSessionToken = connect_xmlhttprequest.getResponseHeader(TUNNEL_TOKEN_HEADER);
// Fail connect attempt if token is not successfully assigned
if (!tunnelSessionToken) {
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
return;
}
// Mark as open // Mark as open
tunnel.setState(Guacamole.Tunnel.State.OPEN); tunnel.setState(Guacamole.Tunnel.State.OPEN);

View File

@@ -58,7 +58,8 @@ class GuacamoleHTTPTunnelMap {
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
/** /**
* Map of all tunnels that are using HTTP, indexed by tunnel UUID. * Map of all tunnels that are using HTTP, indexed by their tunnel-specific
* session tokens.
*/ */
private final ConcurrentMap<String, GuacamoleHTTPTunnel> tunnelMap = private final ConcurrentMap<String, GuacamoleHTTPTunnel> tunnelMap =
new ConcurrentHashMap<String, GuacamoleHTTPTunnel>(); new ConcurrentHashMap<String, GuacamoleHTTPTunnel>();
@@ -141,22 +142,22 @@ class GuacamoleHTTPTunnelMap {
} }
/** /**
* Returns the GuacamoleTunnel having the given UUID, wrapped within a * Returns the GuacamoleTunnel associated with the given tunnel-specific
* GuacamoleHTTPTunnel. If the no tunnel having the given UUID is * session token, wrapped within a GuacamoleHTTPTunnel. If the no tunnel
* available, null is returned. * is associated with the given token, null is returned.
* *
* @param uuid * @param tunnelSessionToken
* The UUID of the tunnel to retrieve. * The tunnel-specific session token of the HTTP tunnel to retrieve.
* *
* @return * @return
* The GuacamoleTunnel having the given UUID, wrapped within a * The GuacamoleTunnel associated with the given tunnel-specific
* GuacamoleHTTPTunnel, if such a tunnel exists, or null if there is no * session token, wrapped within a GuacamoleHTTPTunnel, if such a
* such tunnel. * tunnel exists, or null if there is no such tunnel.
*/ */
public GuacamoleHTTPTunnel get(String uuid) { public GuacamoleHTTPTunnel get(String tunnelSessionToken) {
// Update the last access time // Update the last access time
GuacamoleHTTPTunnel tunnel = tunnelMap.get(uuid); GuacamoleHTTPTunnel tunnel = tunnelMap.get(tunnelSessionToken);
if (tunnel != null) if (tunnel != null)
tunnel.access(); tunnel.access();
@@ -169,32 +170,34 @@ class GuacamoleHTTPTunnelMap {
* Registers that a new connection has been established using HTTP via the * Registers that a new connection has been established using HTTP via the
* given GuacamoleTunnel. * given GuacamoleTunnel.
* *
* @param uuid * @param tunnelSessionToken
* The UUID of the tunnel being added (registered). * The tunnel-specific session token of the HTTP tunnel being added
* (registered).
* *
* @param tunnel * @param tunnel
* The GuacamoleTunnel being registered, its associated connection * The GuacamoleTunnel being registered, its associated connection
* having just been established via HTTP. * having just been established via HTTP.
*/ */
public void put(String uuid, GuacamoleTunnel tunnel) { public void put(String tunnelSessionToken, GuacamoleTunnel tunnel) {
tunnelMap.put(uuid, new GuacamoleHTTPTunnel(tunnel)); tunnelMap.put(tunnelSessionToken, new GuacamoleHTTPTunnel(tunnel));
} }
/** /**
* Removes the GuacamoleTunnel having the given UUID, if such a tunnel * Removes the GuacamoleTunnel associated with the given tunnel-specific
* exists. The original tunnel is returned wrapped within a * session token, if such a tunnel exists. The original tunnel is returned
* GuacamoleHTTPTunnel. * wrapped within a GuacamoleHTTPTunnel.
* *
* @param uuid * @param tunnelSessionToken
* The UUID of the tunnel to remove (deregister). * The tunnel-specific session token of the HTTP tunnel to remove
* (deregister).
* *
* @return * @return
* The GuacamoleTunnel having the given UUID, if such a tunnel exists, * The GuacamoleTunnel having the given tunnel-specific session token,
* wrapped within a GuacamoleHTTPTunnel, or null if no such tunnel * if such a tunnel exists, wrapped within a GuacamoleHTTPTunnel, or
* exists and no removal was performed. * null if no such tunnel exists and no removal was performed.
*/ */
public GuacamoleHTTPTunnel remove(String uuid) { public GuacamoleHTTPTunnel remove(String tunnelSessionToken) {
return tunnelMap.remove(uuid); return tunnelMap.remove(tunnelSessionToken);
} }
/** /**

View File

@@ -25,6 +25,8 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.Reader; import java.io.Reader;
import java.io.Writer; import java.io.Writer;
import java.security.SecureRandom;
import java.util.Base64;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -37,7 +39,6 @@ import org.apache.guacamole.GuacamoleServerException;
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.GuacamoleStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -53,10 +54,17 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
private final Logger logger = LoggerFactory.getLogger(GuacamoleHTTPTunnelServlet.class); private final Logger logger = LoggerFactory.getLogger(GuacamoleHTTPTunnelServlet.class);
/** /**
* Map of absolutely all active tunnels using HTTP, indexed by tunnel UUID. * Map of absolutely all active tunnels using HTTP, indexed by tunnel
* session token.
*/ */
private final GuacamoleHTTPTunnelMap tunnels = new GuacamoleHTTPTunnelMap(); private final GuacamoleHTTPTunnelMap tunnels = new GuacamoleHTTPTunnelMap();
/**
* The name of the HTTP header that contains the tunnel-specific session
* token identifying each active and distinct HTTP tunnel connection.
*/
private static final String TUNNEL_TOKEN_HEADER_NAME = "Guacamole-Tunnel-Token";
/** /**
* The prefix of the query string which denotes a tunnel read operation. * The prefix of the query string which denotes a tunnel read operation.
*/ */
@@ -68,29 +76,64 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
private static final String WRITE_PREFIX = "write:"; private static final String WRITE_PREFIX = "write:";
/** /**
* The length of the read prefix, in characters. * Instance of SecureRandom for generating the session token specific to
* each distinct HTTP tunnel connection.
*/ */
private static final int READ_PREFIX_LENGTH = READ_PREFIX.length(); private final SecureRandom secureRandom = new SecureRandom();
/** /**
* The length of the write prefix, in characters. * Instance of Base64.Encoder for encoding random session tokens as
* strings.
*/ */
private static final int WRITE_PREFIX_LENGTH = WRITE_PREFIX.length(); private final Base64.Encoder encoder = Base64.getEncoder();
/** /**
* The length of every tunnel UUID, in characters. * Generates a new, securely-random session token that may be used to
* represent the ongoing communication session of a distinct HTTP tunnel
* connection.
*
* @return
* A new, securely-random session token.
*/ */
private static final int UUID_LENGTH = 36; protected String generateToken() {
byte[] bytes = new byte[33];
secureRandom.nextBytes(bytes);
return encoder.encodeToString(bytes);
}
/** /**
* Registers the given tunnel such that future read/write requests to that * Registers the given tunnel such that future read/write requests to that
* tunnel will be properly directed. * tunnel will be properly directed.
* *
* @deprecated
* This function has been deprecated in favor of {@link #registerTunnel(java.lang.String, org.apache.guacamole.net.GuacamoleTunnel)},
* which decouples identification of HTTP tunnel sessions from the
* tunnel UUID.
*
* @param tunnel * @param tunnel
* The tunnel to register. * The tunnel to register.
*/ */
@Deprecated
protected void registerTunnel(GuacamoleTunnel tunnel) { protected void registerTunnel(GuacamoleTunnel tunnel) {
tunnels.put(tunnel.getUUID().toString(), tunnel); registerTunnel(tunnel.getUUID().toString(), tunnel);
}
/**
* Registers the given HTTP tunnel such that future read/write requests
* including the given tunnel-specific session token will be properly
* directed. The session token must be unpredictable (securely-random) and
* unique across all active HTTP tunnels. It is recommended that each HTTP
* tunnel session token be obtained through calling {@link #generateToken()}.
*
* @param tunnelSessionToken
* The tunnel-specific session token to associate with the HTTP tunnel
* being registered.
*
* @param tunnel
* The tunnel to register.
*/
protected void registerTunnel(String tunnelSessionToken, GuacamoleTunnel tunnel) {
tunnels.put(tunnelSessionToken, tunnel);
logger.debug("Registered tunnel \"{}\".", tunnel.getUUID()); logger.debug("Registered tunnel \"{}\".", tunnel.getUUID());
} }
@@ -98,33 +141,56 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
* Deregisters the given tunnel such that future read/write requests to * Deregisters the given tunnel such that future read/write requests to
* that tunnel will be rejected. * that tunnel will be rejected.
* *
* @deprecated
* This function has been deprecated in favor of {@link #deregisterTunnel(java.lang.String)},
* which decouples identification of HTTP tunnel sessions from the
* tunnel UUID.
*
* @param tunnel * @param tunnel
* The tunnel to deregister. * The tunnel to deregister.
*/ */
@Deprecated
protected void deregisterTunnel(GuacamoleTunnel tunnel) { protected void deregisterTunnel(GuacamoleTunnel tunnel) {
tunnels.remove(tunnel.getUUID().toString()); deregisterTunnel(tunnel.getUUID().toString());
logger.debug("Deregistered tunnel \"{}\".", tunnel.getUUID());
} }
/** /**
* Returns the tunnel with the given UUID, if it has been registered with * Deregisters the HTTP tunnel associated with the given tunnel-specific
* registerTunnel() and not yet deregistered with deregisterTunnel(). * session token such that future read/write requests to that tunnel will
* be rejected. Each HTTP tunnel must be associated with a session token
* unique to that tunnel via a call {@link #registerTunnel(java.lang.String, org.apache.guacamole.net.GuacamoleTunnel)}.
*
* @param tunnelSessionToken
* The tunnel-specific session token associated with the HTTP tunnel
* being deregistered.
*/
protected void deregisterTunnel(String tunnelSessionToken) {
GuacamoleTunnel tunnel = tunnels.remove(tunnelSessionToken);
if (tunnel != null)
logger.debug("Deregistered tunnel \"{}\".", tunnel.getUUID());
}
/**
* Returns the tunnel associated with the given tunnel-specific session
* token, if it has been registered with {@link #registerTunnel(java.lang.String, org.apache.guacamole.net.GuacamoleTunnel)}
* and not yet deregistered with {@link #deregisterTunnel(java.lang.String)}.
* *
* @param tunnelUUID * @param tunnelSessionToken
* The UUID of registered tunnel. * The tunnel-specific session token associated with the HTTP tunnel to
* be retrieved.
* *
* @return * @return
* The tunnel corresponding to the given UUID. * The tunnel corresponding to the given session token.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If the requested tunnel does not exist because it has not yet been * If the requested tunnel does not exist because it has not yet been
* registered or it has been deregistered. * registered or it has been deregistered.
*/ */
protected GuacamoleTunnel getTunnel(String tunnelUUID) protected GuacamoleTunnel getTunnel(String tunnelSessionToken)
throws GuacamoleException { throws GuacamoleException {
// Pull tunnel from map // Pull tunnel from map
GuacamoleTunnel tunnel = tunnels.get(tunnelUUID); GuacamoleTunnel tunnel = tunnels.get(tunnelSessionToken);
if (tunnel == null) if (tunnel == null)
throw new GuacamoleResourceNotFoundException("No such tunnel."); throw new GuacamoleResourceNotFoundException("No such tunnel.");
@@ -209,52 +275,54 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
if (query == null) if (query == null)
throw new GuacamoleClientException("No query string provided."); throw new GuacamoleClientException("No query string provided.");
// If connect operation, call doConnect() and return tunnel UUID // If connect operation, call doConnect() and return tunnel
// in response. // session token and UUID in response
if (query.equals("connect")) { if (query.equals("connect")) {
GuacamoleTunnel tunnel = doConnect(request); GuacamoleTunnel tunnel = doConnect(request);
if (tunnel != null) { if (tunnel == null)
throw new GuacamoleResourceNotFoundException("No tunnel created.");
// Register newly-created tunnel // Register newly-created tunnel
registerTunnel(tunnel); String tunnelSessionToken = generateToken();
registerTunnel(tunnelSessionToken, tunnel);
try { try {
// Ensure buggy browsers do not cache response // Ensure buggy browsers do not cache response
response.setHeader("Cache-Control", "no-cache"); response.setHeader("Cache-Control", "no-cache");
// Send UUID to client
response.getWriter().print(tunnel.getUUID().toString());
}
catch (IOException e) {
throw new GuacamoleServerException(e);
}
// Include tunnel session token for future requests
response.setHeader(TUNNEL_TOKEN_HEADER_NAME, tunnelSessionToken);
// Send UUID to client
response.getWriter().print(tunnel.getUUID().toString());
}
catch (IOException e) {
throw new GuacamoleServerException(e);
} }
// Failed to connect // Connection successful
else return;
throw new GuacamoleResourceNotFoundException("No tunnel created.");
} }
// If read operation, call doRead() with tunnel UUID, ignoring any // Pull tunnel-specific session token from request
// characters following the tunnel UUID. String tunnelSessionToken = request.getHeader(TUNNEL_TOKEN_HEADER_NAME);
else if (query.startsWith(READ_PREFIX)) if (tunnelSessionToken == null)
doRead(request, response, query.substring( throw new GuacamoleClientException("The HTTP tunnel session "
READ_PREFIX_LENGTH, + "token is required for all requests after "
READ_PREFIX_LENGTH + UUID_LENGTH)); + "connecting.");
// If write operation, call doWrite() with tunnel UUID, ignoring any // Dispatch valid tunnel read/write operations
// characters following the tunnel UUID. if (query.startsWith(READ_PREFIX))
doRead(request, response, tunnelSessionToken);
else if (query.startsWith(WRITE_PREFIX)) else if (query.startsWith(WRITE_PREFIX))
doWrite(request, response, query.substring( doWrite(request, response, tunnelSessionToken);
WRITE_PREFIX_LENGTH,
WRITE_PREFIX_LENGTH + UUID_LENGTH));
// Otherwise, invalid operation // Otherwise, invalid operation
else else
throw new GuacamoleClientException("Invalid tunnel operation: " + query); throw new GuacamoleClientException("Invalid tunnel operation: " + query);
} }
// Catch any thrown guacamole exception and attempt to pass within the // Catch any thrown guacamole exception and attempt to pass within the
@@ -308,20 +376,20 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
* Any data to be sent to the client in response to the write request * Any data to be sent to the client in response to the write request
* should be written to the response body of this HttpServletResponse. * should be written to the response body of this HttpServletResponse.
* *
* @param tunnelUUID * @param tunnelSessionToken
* The UUID of the tunnel to read from, as specified in the write * The tunnel-specific session token of the HTTP tunnel to read from,
* request. This tunnel must have been created by a previous call to * as specified in the read request. This tunnel must have been created
* doConnect(). * by a previous call to doConnect().
* *
* @throws GuacamoleException * @throws GuacamoleException
* If an error occurs while handling the read request. * If an error occurs while handling the read request.
*/ */
protected void doRead(HttpServletRequest request, protected void doRead(HttpServletRequest request,
HttpServletResponse response, String tunnelUUID) HttpServletResponse response, String tunnelSessionToken)
throws GuacamoleException { throws GuacamoleException {
// Get tunnel, ensure tunnel exists // Get tunnel, ensure tunnel exists
GuacamoleTunnel tunnel = getTunnel(tunnelUUID); GuacamoleTunnel tunnel = getTunnel(tunnelSessionToken);
// Ensure tunnel is open // Ensure tunnel is open
if (!tunnel.isOpen()) if (!tunnel.isOpen())
@@ -371,7 +439,7 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
// Close tunnel immediately upon EOF // Close tunnel immediately upon EOF
if (message == null) { if (message == null) {
deregisterTunnel(tunnel); deregisterTunnel(tunnelSessionToken);
tunnel.close(); tunnel.close();
} }
@@ -385,7 +453,7 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
catch (GuacamoleConnectionClosedException e) { catch (GuacamoleConnectionClosedException e) {
// Deregister and close // Deregister and close
deregisterTunnel(tunnel); deregisterTunnel(tunnelSessionToken);
tunnel.close(); tunnel.close();
// End-of-instructions marker // End-of-instructions marker
@@ -398,7 +466,7 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
catch (GuacamoleException e) { catch (GuacamoleException e) {
// Deregister and close // Deregister and close
deregisterTunnel(tunnel); deregisterTunnel(tunnelSessionToken);
tunnel.close(); tunnel.close();
throw e; throw e;
@@ -416,7 +484,7 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
logger.debug("Error writing to servlet output stream", e); logger.debug("Error writing to servlet output stream", e);
// Deregister and close // Deregister and close
deregisterTunnel(tunnel); deregisterTunnel(tunnelSessionToken);
tunnel.close(); tunnel.close();
} }
@@ -439,19 +507,19 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
* @param response * @param response
* The HttpServletResponse associated with the write request received. * The HttpServletResponse associated with the write request received.
* *
* @param tunnelUUID * @param tunnelSessionToken
* The UUID of the tunnel to write to, as specified in the write * The tunnel-specific session token of the HTTP tunnel to write to,
* request. This tunnel must have been created by a previous call to * as specified in the write request. This tunnel must have been created
* doConnect(). * by a previous call to doConnect().
* *
* @throws GuacamoleException * @throws GuacamoleException
* If an error occurs while handling the write request. * If an error occurs while handling the write request.
*/ */
protected void doWrite(HttpServletRequest request, protected void doWrite(HttpServletRequest request,
HttpServletResponse response, String tunnelUUID) HttpServletResponse response, String tunnelSessionToken)
throws GuacamoleException { throws GuacamoleException {
GuacamoleTunnel tunnel = getTunnel(tunnelUUID); GuacamoleTunnel tunnel = getTunnel(tunnelSessionToken);
// We still need to set the content type to avoid the default of // We still need to set the content type to avoid the default of
// text/html, as such a content type would cause some browsers to // text/html, as such a content type would cause some browsers to
@@ -498,7 +566,7 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet {
catch (IOException e) { catch (IOException e) {
// Deregister and close // Deregister and close
deregisterTunnel(tunnel); deregisterTunnel(tunnelSessionToken);
tunnel.close(); tunnel.close();
throw new GuacamoleServerException("I/O Error sending data to server: " + e.getMessage(), e); throw new GuacamoleServerException("I/O Error sending data to server: " + e.getMessage(), e);