/* * 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.servlet; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleConnectionClosedException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.io.GuacamoleReader; import org.apache.guacamole.io.GuacamoleWriter; import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.protocol.GuacamoleStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A HttpServlet implementing and abstracting the operations required by the * HTTP implementation of the JavaScript Guacamole client's tunnel. * * @author Michael Jumper */ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(GuacamoleHTTPTunnelServlet.class); /** * Map of absolutely all active tunnels using HTTP, indexed by tunnel UUID. */ private final GuacamoleHTTPTunnelMap tunnels = new GuacamoleHTTPTunnelMap(); /** * The prefix of the query string which denotes a tunnel read operation. */ private static final String READ_PREFIX = "read:"; /** * The prefix of the query string which denotes a tunnel write operation. */ private static final String WRITE_PREFIX = "write:"; /** * The length of the read prefix, in characters. */ private static final int READ_PREFIX_LENGTH = READ_PREFIX.length(); /** * The length of the write prefix, in characters. */ private static final int WRITE_PREFIX_LENGTH = WRITE_PREFIX.length(); /** * The length of every tunnel UUID, in characters. */ private static final int UUID_LENGTH = 36; /** * Registers the given tunnel such that future read/write requests to that * tunnel will be properly directed. * * @param tunnel * The tunnel to register. */ protected void registerTunnel(GuacamoleTunnel tunnel) { tunnels.put(tunnel.getUUID().toString(), tunnel); logger.debug("Registered tunnel \"{}\".", tunnel.getUUID()); } /** * Deregisters the given tunnel such that future read/write requests to * that tunnel will be rejected. * * @param tunnel * The tunnel to deregister. */ protected void deregisterTunnel(GuacamoleTunnel tunnel) { tunnels.remove(tunnel.getUUID().toString()); logger.debug("Deregistered tunnel \"{}\".", tunnel.getUUID()); } /** * Returns the tunnel with the given UUID, if it has been registered with * registerTunnel() and not yet deregistered with deregisterTunnel(). * * @param tunnelUUID * The UUID of registered tunnel. * * @return * The tunnel corresponding to the given UUID. * * @throws GuacamoleException * If the requested tunnel does not exist because it has not yet been * registered or it has been deregistered. */ protected GuacamoleTunnel getTunnel(String tunnelUUID) throws GuacamoleException { // Pull tunnel from map GuacamoleTunnel tunnel = tunnels.get(tunnelUUID); if (tunnel == null) throw new GuacamoleResourceNotFoundException("No such tunnel."); return tunnel; } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException { handleTunnelRequest(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException { handleTunnelRequest(request, response); } /** * Sends an error on the given HTTP response using the information within * the given GuacamoleStatus. * * @param response * The HTTP response to use to send the error. * * @param guacStatus * The status to send * * @param message * A human-readable message that can be presented to the user. * * @throws ServletException * If an error prevents sending of the error code. */ protected void sendError(HttpServletResponse response, GuacamoleStatus guacStatus, String message) throws ServletException { try { // If response not committed, send error code and message if (!response.isCommitted()) { response.addHeader("Guacamole-Status-Code", Integer.toString(guacStatus.getGuacamoleStatusCode())); response.addHeader("Guacamole-Error-Message", message); response.sendError(guacStatus.getHttpStatusCode()); } } catch (IOException ioe) { // If unable to send error at all due to I/O problems, // rethrow as servlet exception throw new ServletException(ioe); } } /** * Dispatches every HTTP GET and POST request to the appropriate handler * function based on the query string. * * @param request * The HttpServletRequest associated with the GET or POST request * received. * * @param response * The HttpServletResponse associated with the GET or POST request * received. * * @throws ServletException * If an error occurs while servicing the request. */ protected void handleTunnelRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException { try { String query = request.getQueryString(); if (query == null) throw new GuacamoleClientException("No query string provided."); // If connect operation, call doConnect() and return tunnel UUID // in response. if (query.equals("connect")) { GuacamoleTunnel tunnel = doConnect(request); if (tunnel != null) { // Register newly-created tunnel registerTunnel(tunnel); try { // Ensure buggy browsers do not cache response response.setHeader("Cache-Control", "no-cache"); // Send UUID to client response.getWriter().print(tunnel.getUUID().toString()); } catch (IOException e) { throw new GuacamoleServerException(e); } } // Failed to connect else throw new GuacamoleResourceNotFoundException("No tunnel created."); } // If read operation, call doRead() with tunnel UUID, ignoring any // characters following the tunnel UUID. else if(query.startsWith(READ_PREFIX)) doRead(request, response, query.substring( READ_PREFIX_LENGTH, READ_PREFIX_LENGTH + UUID_LENGTH)); // If write operation, call doWrite() with tunnel UUID, ignoring any // characters following the tunnel UUID. else if(query.startsWith(WRITE_PREFIX)) doWrite(request, response, query.substring( WRITE_PREFIX_LENGTH, WRITE_PREFIX_LENGTH + UUID_LENGTH)); // Otherwise, invalid operation else throw new GuacamoleClientException("Invalid tunnel operation: " + query); } // Catch any thrown guacamole exception and attempt to pass within the // HTTP response, logging each error appropriately. catch (GuacamoleClientException e) { logger.warn("HTTP tunnel request rejected: {}", e.getMessage()); sendError(response, e.getStatus(), e.getMessage()); } catch (GuacamoleException e) { logger.error("HTTP tunnel request failed: {}", e.getMessage()); logger.debug("Internal error in HTTP tunnel.", e); sendError(response, e.getStatus(), "Internal server error."); } } /** * Called whenever the JavaScript Guacamole client makes a connection * request via HTTP. It it up to the implementor of this function to define * what conditions must be met for a tunnel to be configured and returned * as a result of this connection request (whether some sort of credentials * must be specified, for example). * * @param request * The HttpServletRequest associated with the connection request * received. Any parameters specified along with the connection request * can be read from this object. * * @return * A newly constructed GuacamoleTunnel if successful, null otherwise. * * @throws GuacamoleException * If an error occurs while constructing the GuacamoleTunnel, or if the * conditions required for connection are not met. */ protected abstract GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException; /** * Called whenever the JavaScript Guacamole client makes a read request. * This function should in general not be overridden, as it already * contains a proper implementation of the read operation. * * @param request * The HttpServletRequest associated with the read request received. * * @param response * The HttpServletResponse associated with the write request received. * Any data to be sent to the client in response to the write request * should be written to the response body of this HttpServletResponse. * * @param tunnelUUID * The UUID of the tunnel to read from, as specified in the write * request. This tunnel must have been created by a previous call to * doConnect(). * * @throws GuacamoleException * If an error occurs while handling the read request. */ protected void doRead(HttpServletRequest request, HttpServletResponse response, String tunnelUUID) throws GuacamoleException { // Get tunnel, ensure tunnel exists GuacamoleTunnel tunnel = getTunnel(tunnelUUID); // Ensure tunnel is open if (!tunnel.isOpen()) throw new GuacamoleResourceNotFoundException("Tunnel is closed."); // Obtain exclusive read access GuacamoleReader reader = tunnel.acquireReader(); try { // Note that although we are sending text, Webkit browsers will // buffer 1024 bytes before starting a normal stream if we use // anything but application/octet-stream. response.setContentType("application/octet-stream"); response.setHeader("Cache-Control", "no-cache"); // Get writer for response Writer out = new BufferedWriter(new OutputStreamWriter( response.getOutputStream(), "UTF-8")); // Stream data to response, ensuring output stream is closed try { // Deregister tunnel and throw error if we reach EOF without // having ever sent any data char[] message = reader.read(); if (message == null) throw new GuacamoleConnectionClosedException("Tunnel reached end of stream."); // For all messages, until another stream is ready (we send at least one message) do { // Get message output bytes out.write(message, 0, message.length); // Flush if we expect to wait if (!reader.available()) { out.flush(); response.flushBuffer(); } // No more messages another stream can take over if (tunnel.hasQueuedReaderThreads()) break; } while (tunnel.isOpen() && (message = reader.read()) != null); // Close tunnel immediately upon EOF if (message == null) { deregisterTunnel(tunnel); tunnel.close(); } // End-of-instructions marker out.write("0.;"); out.flush(); response.flushBuffer(); } // Send end-of-stream marker and close tunnel if connection is closed catch (GuacamoleConnectionClosedException e) { // Deregister and close deregisterTunnel(tunnel); tunnel.close(); // End-of-instructions marker out.write("0.;"); out.flush(); response.flushBuffer(); } catch (GuacamoleException e) { // Deregister and close deregisterTunnel(tunnel); tunnel.close(); throw e; } // Always close output stream finally { out.close(); } } catch (IOException e) { // Log typically frequent I/O error if desired logger.debug("Error writing to servlet output stream", e); // Deregister and close deregisterTunnel(tunnel); tunnel.close(); } finally { tunnel.releaseReader(); } } /** * Called whenever the JavaScript Guacamole client makes a write request. * This function should in general not be overridden, as it already * contains a proper implementation of the write operation. * * @param request * The HttpServletRequest associated with the write request received. * Any data to be written will be specified within the body of this * request. * * @param response * The HttpServletResponse associated with the write request received. * * @param tunnelUUID * The UUID of the tunnel to write to, as specified in the write * request. This tunnel must have been created by a previous call to * doConnect(). * * @throws GuacamoleException * If an error occurs while handling the write request. */ protected void doWrite(HttpServletRequest request, HttpServletResponse response, String tunnelUUID) throws GuacamoleException { GuacamoleTunnel tunnel = getTunnel(tunnelUUID); // 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 // attempt to parse the result, even though the JavaScript client // does not explicitly request such parsing. response.setContentType("application/octet-stream"); response.setHeader("Cache-Control", "no-cache"); response.setContentLength(0); // Send data try { // Get writer from tunnel GuacamoleWriter writer = tunnel.acquireWriter(); // Get input reader for HTTP stream Reader input = new InputStreamReader( request.getInputStream(), "UTF-8"); // Transfer data from input stream to tunnel output, ensuring // input is always closed try { // Buffer int length; char[] buffer = new char[8192]; // Transfer data using buffer while (tunnel.isOpen() && (length = input.read(buffer, 0, buffer.length)) != -1) writer.write(buffer, 0, length); } // Close input stream in all cases finally { input.close(); } } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); } catch (IOException e) { // Deregister and close deregisterTunnel(tunnel); tunnel.close(); throw new GuacamoleServerException("I/O Error sending data to server: " + e.getMessage(), e); } finally { tunnel.releaseWriter(); } } @Override public void destroy() { tunnels.shutdown(); } } /** * \example ExampleTunnelServlet.java * * A basic example demonstrating extending GuacamoleTunnelServlet and * implementing doConnect() to configure the Guacamole connection as * desired. */