| /* |
| * 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.protocol; |
| |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Queue; |
| import org.apache.guacamole.GuacamoleException; |
| import org.apache.guacamole.GuacamoleUpstreamException; |
| import org.apache.guacamole.GuacamoleUpstreamNotFoundException; |
| import org.apache.guacamole.GuacamoleUpstreamTimeoutException; |
| import org.apache.guacamole.GuacamoleUpstreamUnavailableException; |
| import org.apache.guacamole.io.GuacamoleReader; |
| import org.apache.guacamole.io.GuacamoleWriter; |
| import org.apache.guacamole.net.GuacamoleSocket; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * GuacamoleSocket which intercepts errors received early in the Guacamole |
| * session. Upstream errors which are intercepted early enough result in |
| * exceptions thrown immediately within the FailoverGuacamoleSocket's |
| * constructor, allowing a different socket to be substituted prior to |
| * fulfilling the connection. |
| */ |
| public class FailoverGuacamoleSocket implements GuacamoleSocket { |
| |
| /** |
| * Logger for this class. |
| */ |
| private static final Logger logger = |
| LoggerFactory.getLogger(FailoverGuacamoleSocket.class); |
| |
| /** |
| * The maximum number of characters of Guacamole instruction data to store |
| * within the instruction queue while searching for errors. Once this limit |
| * is exceeded, the connection is assumed to be successful. |
| */ |
| private static final int INSTRUCTION_QUEUE_LIMIT = 2048; |
| |
| /** |
| * The wrapped socket being used. |
| */ |
| private final GuacamoleSocket socket; |
| |
| /** |
| * Queue of all instructions read while this FailoverGuacamoleSocket was |
| * being constructed. |
| */ |
| private final Queue<GuacamoleInstruction> instructionQueue = |
| new LinkedList<GuacamoleInstruction>(); |
| |
| /** |
| * Parses the given "error" instruction, throwing an exception if the |
| * instruction represents an error from the upstream remote desktop. |
| * |
| * @param instruction |
| * The "error" instruction to parse. |
| * |
| * @throws GuacamoleUpstreamException |
| * If the "error" instruction represents an error from the upstream |
| * remote desktop. |
| */ |
| private static void handleUpstreamErrors(GuacamoleInstruction instruction) |
| throws GuacamoleUpstreamException { |
| |
| // Ignore error instructions which are missing the status code |
| List<String> args = instruction.getArgs(); |
| if (args.size() < 2) { |
| logger.debug("Received \"error\" instruction without status code."); |
| return; |
| } |
| |
| // Parse the status code from the received error instruction |
| int statusCode; |
| try { |
| statusCode = Integer.parseInt(args.get(1)); |
| } |
| catch (NumberFormatException e) { |
| logger.debug("Received \"error\" instruction with non-numeric status code.", e); |
| return; |
| } |
| |
| // Translate numeric status code into a GuacamoleStatus |
| GuacamoleStatus status = GuacamoleStatus.fromGuacamoleStatusCode(statusCode); |
| if (status == null) { |
| logger.debug("Received \"error\" instruction with unknown/invalid status code: {}", statusCode); |
| return; |
| } |
| |
| // Only handle error instructions related to the upstream remote desktop |
| switch (status) { |
| |
| // Generic upstream error |
| case UPSTREAM_ERROR: |
| throw new GuacamoleUpstreamException(args.get(0)); |
| |
| // Upstream is unreachable |
| case UPSTREAM_NOT_FOUND: |
| throw new GuacamoleUpstreamNotFoundException(args.get(0)); |
| |
| // Upstream did not respond |
| case UPSTREAM_TIMEOUT: |
| throw new GuacamoleUpstreamTimeoutException(args.get(0)); |
| |
| // Upstream is refusing the connection |
| case UPSTREAM_UNAVAILABLE: |
| throw new GuacamoleUpstreamUnavailableException(args.get(0)); |
| |
| } |
| |
| } |
| |
| /** |
| * Creates a new FailoverGuacamoleSocket which reads Guacamole instructions |
| * from the given socket, searching for errors from the upstream remote |
| * desktop. If an upstream error is encountered, it is thrown as a |
| * GuacamoleUpstreamException. This constructor will block until an error |
| * is encountered, or until the connection appears to have been successful. |
| * Once the FailoverGuacamoleSocket has been created, all reads, writes, |
| * etc. will be delegated to the provided socket. |
| * |
| * @param socket |
| * The GuacamoleSocket of the Guacamole connection this |
| * FailoverGuacamoleSocket should handle. |
| * |
| * @throws GuacamoleException |
| * If an error occurs while reading data from the provided socket. |
| * |
| * @throws GuacamoleUpstreamException |
| * If the connection to guacd succeeded, but an error occurred while |
| * connecting to the remote desktop. |
| */ |
| public FailoverGuacamoleSocket(GuacamoleSocket socket) |
| throws GuacamoleException, GuacamoleUpstreamException { |
| |
| int totalQueueSize = 0; |
| |
| GuacamoleInstruction instruction; |
| GuacamoleReader reader = socket.getReader(); |
| |
| // Continuously read instructions, searching for errors |
| while ((instruction = reader.readInstruction()) != null) { |
| |
| // Add instruction to tail of instruction queue |
| instructionQueue.add(instruction); |
| |
| // If instruction is a "sync" instruction, stop reading |
| String opcode = instruction.getOpcode(); |
| if (opcode.equals("sync")) |
| break; |
| |
| // If instruction is an "error" instruction, parse its contents and |
| // stop reading |
| if (opcode.equals("error")) { |
| handleUpstreamErrors(instruction); |
| break; |
| } |
| |
| // Otherwise, track total data parsed, and assume connection is |
| // successful if no error encountered within reasonable space |
| totalQueueSize += instruction.toString().length(); |
| if (totalQueueSize >= INSTRUCTION_QUEUE_LIMIT) |
| break; |
| |
| } |
| |
| this.socket = socket; |
| |
| } |
| |
| /** |
| * GuacamoleReader which reads instructions from the queue populated when |
| * the FailoverGuacamoleSocket was constructed. Once the queue has been |
| * emptied, reads are delegated directly to the reader of the wrapped |
| * socket. |
| */ |
| private final GuacamoleReader queuedReader = new GuacamoleReader() { |
| |
| @Override |
| public boolean available() throws GuacamoleException { |
| return !instructionQueue.isEmpty() || socket.getReader().available(); |
| } |
| |
| @Override |
| public char[] read() throws GuacamoleException { |
| |
| // Read instructions from queue before finally delegating to |
| // underlying reader (received when FailoverGuacamoleSocket was |
| // being constructed) |
| if (!instructionQueue.isEmpty()) { |
| GuacamoleInstruction instruction = instructionQueue.remove(); |
| return instruction.toString().toCharArray(); |
| } |
| |
| return socket.getReader().read(); |
| |
| } |
| |
| @Override |
| public GuacamoleInstruction readInstruction() |
| throws GuacamoleException { |
| |
| // Read instructions from queue before finally delegating to |
| // underlying reader (received when FailoverGuacamoleSocket was |
| // being constructed) |
| if (!instructionQueue.isEmpty()) |
| return instructionQueue.remove(); |
| |
| return socket.getReader().readInstruction(); |
| |
| } |
| |
| }; |
| |
| @Override |
| public GuacamoleReader getReader() { |
| return queuedReader; |
| } |
| |
| @Override |
| public GuacamoleWriter getWriter() { |
| return socket.getWriter(); |
| } |
| |
| @Override |
| public void close() throws GuacamoleException { |
| socket.close(); |
| } |
| |
| @Override |
| public boolean isOpen() { |
| return socket.isOpen(); |
| } |
| |
| } |