blob: f8e0334166aafb101ec72025d6a63a00ed870b64 [file] [log] [blame]
/*
* 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.tunnel;
import com.google.common.io.BaseEncoding;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.protocol.GuacamoleInstruction;
import org.apache.guacamole.protocol.GuacamoleStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Filter which selectively intercepts "ack" instructions, automatically reading
* from or closing the stream given with interceptStream(). The required "blob"
* and "end" instructions denoting the content and boundary of the stream are
* sent automatically.
*/
public class InputStreamInterceptingFilter
extends StreamInterceptingFilter<InputStream> {
/**
* Logger for this class.
*/
private static final Logger logger =
LoggerFactory.getLogger(InputStreamInterceptingFilter.class);
/**
* Creates a new InputStreamInterceptingFilter which selectively intercepts
* "ack" instructions. The required "blob" and "end" instructions will
* automatically be sent over the given tunnel based on the content of
* provided InputStreams.
*
* @param tunnel
* The GuacamoleTunnel over which any required "blob" and "end"
* instructions should be sent.
*/
public InputStreamInterceptingFilter(GuacamoleTunnel tunnel) {
super(tunnel);
}
/**
* Injects a "blob" instruction into the outbound Guacamole protocol
* stream, as if sent by the connected client. "blob" instructions are used
* to send chunks of data along a stream.
*
* @param index
* The index of the stream that this "blob" instruction relates to.
*
* @param blob
* The chunk of data to send within the "blob" instruction.
*/
private void sendBlob(String index, byte[] blob) {
// Send "blob" containing provided data
sendInstruction(new GuacamoleInstruction("blob", index,
BaseEncoding.base64().encode(blob)));
}
/**
* Injects an "end" instruction into the outbound Guacamole protocol
* stream, as if sent by the connected client. "end" instructions are used
* to signal the end of a stream.
*
* @param index
* The index of the stream that this "end" instruction relates to.
*/
private void sendEnd(String index) {
sendInstruction(new GuacamoleInstruction("end", index));
}
/**
* Reads the next chunk of data from the InputStream associated with an
* intercepted stream, sending that data as a "blob" instruction over the
* GuacamoleTunnel associated with this filter. If the end of the
* InputStream is reached, an "end" instruction will automatically be sent.
*
* @param stream
* The stream from which the next chunk of data should be read.
*/
private void readNextBlob(InterceptedStream<InputStream> stream) {
// Read blob from stream if it exists
try {
// Read raw data from input stream
byte[] blob = new byte[6048];
int length = stream.getStream().read(blob);
// End stream if no more data
if (length == -1) {
// Close stream, send end if the stream is still valid
if (closeInterceptedStream(stream))
sendEnd(stream.getIndex());
return;
}
// Inject corresponding "blob" instruction
sendBlob(stream.getIndex(), Arrays.copyOf(blob, length));
}
// Terminate stream if it cannot be read
catch (IOException e) {
logger.debug("Unable to read data of intercepted input stream.", e);
// Close stream, send end if the stream is still valid
if (closeInterceptedStream(stream))
sendEnd(stream.getIndex());
}
}
/**
* Handles a single "ack" instruction, sending yet more blobs or closing the
* stream depending on whether the "ack" indicates success or failure. If no
* InputStream is associated with the stream index within the "ack"
* instruction, the instruction is ignored.
*
* @param instruction
* The "ack" instruction being handled.
*/
private void handleAck(GuacamoleInstruction instruction) {
// Verify all required arguments are present
List<String> args = instruction.getArgs();
if (args.size() < 3)
return;
// Pull associated stream
String index = args.get(0);
InterceptedStream<InputStream> stream = getInterceptedStream(index);
if (stream == null)
return;
// Pull status code
String status = args.get(2);
// Terminate stream if an error is encountered
if (!status.equals("0")) {
// Parse status code as integer
int code;
try {
code = Integer.parseInt(status);
}
// Assume internal error if parsing fails
catch (NumberFormatException e) {
logger.debug("Translating invalid status code \"{}\" to SERVER_ERROR.", status);
code = GuacamoleStatus.SERVER_ERROR.getGuacamoleStatusCode();
}
// Flag error and close stream
stream.setStreamError(code, args.get(1));
closeInterceptedStream(stream);
return;
}
// Send next blob
readNextBlob(stream);
}
@Override
public GuacamoleInstruction filter(GuacamoleInstruction instruction)
throws GuacamoleException {
// Intercept "ack" instructions for in-progress streams
if (instruction.getOpcode().equals("ack"))
handleAck(instruction);
// Pass instruction through untouched
return instruction;
}
@Override
protected void handleInterceptedStream(InterceptedStream<InputStream> stream) {
// Read the first blob. Note that future blobs will be read in response
// to received "ack" instructions.
readNextBlob(stream);
}
}