All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.guacamole.servlet.GuacamoleHTTPTunnelServlet Maven / Gradle / Ivy

Go to download

The base Java API of the Guacamole project, providing Java support for the Guacamole stack.

There is a newer version: 1.5.5
Show newest version
/*
 * 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.
 */





© 2015 - 2024 Weber Informatics LLC | Privacy Policy