content: /*
   * 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.
   */
  
  