/* * Copyright 2012-2015, the original author or authors. * * Licensed 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 com.flipkart.phantom.runtime.impl.server.netty.handler.command; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferInputStream; import org.jboss.netty.buffer.ChannelBufferOutputStream; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.ChannelEvent; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.MessageEvent; import org.springframework.util.SerializationUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.flipkart.phantom.task.spi.TaskResult; /** * <code>CommandInterpreter</code> interprets a Command from the Netty {@link MessageEvent} * The command protocol is defined as follows: * * <pre> * Command is described as below * +------------+---------+------------+--------------+---+---------------+------------+--------------+---+---------------+----+ * | delim char | command | delim char | param name 1 | = | param value 1 | delim char | param name n | = | param value n | \n | * +------------+---------+------------+--------------+---+---------------+------------+--------------+---+---------------+----+ * +------------+ * | data bytes | * +------------+ * * where * <ul> * <li>Command and params appear on a single line terminating in '\n' char</li> * <li>'delim char' is any non-ascii character</li> * <li>'command' is an arbitrary sequence of characters</li> * <li>'param name'='param value' can repeat any number of times. Are of type : arbitrary sequence of characters</li> * <li>'data' is an arbitrary sequence of bytes</li> * </ul> * * Response from Command execution is described as below * * +--------+----+ * | status | \n | * +--------+----+ * (or) * +--------+-------------+-------------+----+ * | status | white space | data length | \n | * +--------+-------------+-------------+----+ * +------------+ * | data bytes | * +------------+ * * <pre> * * Command protocol interpretation code is based on the implementation in com.flipkart.w3.agent.W3Agent * * @author Regunath B * @version 1.0, 22 Mar 2013 */ @SuppressWarnings("rawtypes") public class CommandInterpreter { /** Constant for max command input size*/ public static final int MAX_COMMAND_INPUT = 20480; /** Constants for characters that have special meaning in the command protocol*/ public static final char LINE_FEED = '\n'; private static final char CARRIAGE_RETURN = '\r'; private static final char DEFAULT_DELIM = ' '; private static final char PARAM_VALUE_SEP = '='; private static final char[] ASCII_LOW = {'a','z'}; private static final char[] ASCII_HIGH = {'A','Z'}; private static final String SUCCESS = "SUCCESS"; private static final String ERROR = "ERROR"; private static final String NULL_STRING = ""; /** Default param value, when none is specified*/ private static final String DEFAULT_PARAM_VALUE = "true"; /** The Jackson ObjectMapper for writing output as JSON*/ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); // using an instance variable as this class is deemed to be thread-safe /** Enumeration of read failure reasons */ public enum ReadFailure { INSUFFICIENT_DATA, } /** * Helper method to read and return a ProxyCommand from an {@link InputStream}. Throws Exception for all data read errors including partial * reads arising from insufficient data * @param inputStream the InputStream instance * @return the read ProxyCommand * @throws Exception in case of errors */ public ProxyCommand readCommand(InputStream inputStream) throws Exception { return this.interpretCommand(inputStream, true); } /** * Helper method to read and return a ProxyCommand from an input Channel {@link MessageEvent}. Throws Exception for all data read errors including partial * reads arising from insufficient data * @param event the MessageEvent instance * @return the read ProxyCommand * @throws Exception in case of errors */ public ProxyCommand readCommand(MessageEvent event) throws Exception { return this.interpretCommand(new ChannelBufferInputStream((ChannelBuffer)event.getMessage()), true); } /** * Helper method to read and return a ProxyCommand from a ChannelBuffer {@link ChannelBuffer}. Returns a ProxyCommand for partial read errors and throws * Exception only for irrecoverable errors. Useful method to decode data frames from the raw input channel buffer. * @param buffer the input buffer * @return the read ProxyCommand * @throws Exception in case of errors */ public ProxyCommand interpretCommand(ChannelBuffer buffer) throws Exception { return this.interpretCommand(new ChannelBufferInputStream(buffer), false); } /** * Writes the specified TaskResult data to the channel output following the Command protocol * @param ctx the ChannelHandlerContext * @param event the ChannelEvent * @param result the TaskResult data written to the channel response * @throws Exception in case of any errors */ public void writeCommandExecutionResponse(ChannelHandlerContext ctx, ChannelEvent event, TaskResult result) throws Exception { ChannelBuffer writeBuffer = ChannelBuffers.dynamicBuffer(); this.writeCommandExecutionResponse(new ChannelBufferOutputStream(writeBuffer), result); Channels.write(ctx, event.getFuture(), writeBuffer); } /** * Writes the specified TaskResult data to the Outputstream following the Command protocol * @param outputStream the Outputstream to write result data to * @param result the TaskResult to write * @throws Exception in case of any errors */ public void writeCommandExecutionResponse(OutputStream outputStream, TaskResult result) throws Exception { //Don't write anything if the result is null if(result==null) { return; } String message = result.getMessage(); boolean success = result.isSuccess(); int resultDatalength = result.getLength(); String metaContents = (message==null ? (success ? SUCCESS : ERROR) : message); metaContents += (resultDatalength==0 ? LINE_FEED : (DEFAULT_DELIM + NULL_STRING + resultDatalength + NULL_STRING + LINE_FEED)); // write the meta contents outputStream.write(metaContents.getBytes()); // now write the result data if(result.isDataArray()) { for(Object object : result.getDataArray()) { if(object!=null) { if(object instanceof byte[]) { outputStream.write((byte[]) object); } else { OBJECT_MAPPER.writeValue(outputStream, object); } } } } else { byte[] metaData = result.getMetadata(); if(metaData != null && (metaData.length > 0)) { outputStream.write(metaData); } Object data = result.getData(); if(data!=null) { if(data instanceof byte[]) { byte[] byteData = (byte[]) data; if(byteData.length>0) { outputStream.write(byteData); } } else { OBJECT_MAPPER.writeValue(outputStream, data); } } } } /** * Helper method to read and return a ProxyCommand from an input {@link InputStream} * @param inputStream the InputStream instance * @param isFramedTransport boolean indicator that defines mechanism for reporting errors - Exceptions vs a ProxyCommand with error description * @return the read ProxyCommand * @throws Exception in case of errors */ private ProxyCommand interpretCommand(InputStream inputStream, boolean isFramedTransport) throws Exception { ProxyCommand readCommand = null; byte[] readBytes = new byte[MAX_COMMAND_INPUT]; int byteReadIndex=0, commandEndIndex=0, dataStartIndex=0, dataLength=0; while(byteReadIndex < MAX_COMMAND_INPUT) { int bytesRead = inputStream.read(readBytes, byteReadIndex, MAX_COMMAND_INPUT-byteReadIndex); // try to read as much as is available into the byte array if(bytesRead <= 0){ // check if no data was read at all. Throw an IllegalArgumentException to indicate unexpected end of stream if (isFramedTransport) { throw new IllegalArgumentException("Invalid read. Encountered end of stream before reading a single byte"); } else { return new ProxyCommand(ReadFailure.INSUFFICIENT_DATA, "Invalid read. Encountered end of stream before reading a single byte"); } } for(int i=0; i<bytesRead; i++) { // look for the NEW_LINE character that signals end of command and params input if(readBytes[byteReadIndex+i ]== LINE_FEED) { commandEndIndex = byteReadIndex+i; break; } } if (bytesRead > 0) { byteReadIndex += bytesRead; // skip the read bytes by moving the index for next read } if(commandEndIndex > 0 || bytesRead <= 0) { // break the read loop if end of command line is reached (or) EOS (end of stream is reached) break; // i.e. no more data available for read } } if(commandEndIndex==0) { // report a suitable error if NEW_LINE was not encountered at all (or) if bytes read has exceeded MAX_COMMAND_INPUT if(byteReadIndex < MAX_COMMAND_INPUT){ if (isFramedTransport) { throw new IllegalArgumentException("Stream ended before encountering a \\n: " + new String(readBytes,0,byteReadIndex)); } else { return new ProxyCommand(ReadFailure.INSUFFICIENT_DATA, "Stream ended before encountering a \\n: " + new String(readBytes,0,byteReadIndex)); } } else { throw new IllegalArgumentException("Maximum command line size allowed: " + MAX_COMMAND_INPUT +" Command : "+ new String(readBytes,0,byteReadIndex)); } } // The input data appears to adhere to the command protocol. Proceed to read the command, params and data dataStartIndex = commandEndIndex+1; if (readBytes[commandEndIndex-1] == CARRIAGE_RETURN) { commandEndIndex--; // handle the CR for people who still haven't moved on from telnet to netcat } byte delimiter = DEFAULT_DELIM; int fragmentStart = 0; if(!(readBytes[0]>=ASCII_LOW[0] && readBytes[0]<=ASCII_LOW[1]) && !(readBytes[0]>=ASCII_HIGH[0] && readBytes[0]<=ASCII_HIGH[1])) { delimiter = readBytes[0]; // the delimiter is not DEFAULT_DELIM but the non-ascii character appearing as the first byte fragmentStart=1; } int fragmentIndex = this.getNextCommandFragmentPosition(readBytes, fragmentStart, commandEndIndex, delimiter); readCommand = new ProxyCommand(new String(readBytes, fragmentStart, fragmentIndex-fragmentStart)); Map<String, Object> commandParams = new HashMap<>(); // gather params while(fragmentIndex < commandEndIndex) { // skip initial delims while(fragmentIndex < commandEndIndex && readBytes[fragmentIndex] == delimiter) { fragmentIndex++; } if (fragmentIndex == commandEndIndex) { break; } // read first char if(Character.isDigit((char)readBytes[fragmentIndex])) { // this is the datalen try { dataLength = Integer.parseInt(new String(readBytes, fragmentIndex, commandEndIndex-fragmentIndex)); break; } catch (Exception e) { throw new IllegalArgumentException("Invalid syntax in command: "+new String(readBytes), e); } } else { fragmentStart = fragmentIndex; fragmentIndex = getNextCommandFragmentPosition(readBytes, fragmentIndex+1, commandEndIndex, delimiter); int paramValueSepIndex = 0; for(int i=fragmentStart; i<fragmentIndex; i++) { if (readBytes[i] == PARAM_VALUE_SEP) { paramValueSepIndex = i; break; } } if (paramValueSepIndex > 0) { commandParams.put(new String(readBytes, fragmentStart, paramValueSepIndex-fragmentStart), new String(readBytes, paramValueSepIndex+1, fragmentIndex-paramValueSepIndex-1)); } else { commandParams.put(new String(readBytes, fragmentStart, fragmentIndex-fragmentStart), DEFAULT_PARAM_VALUE); // initialize with default value if none specified } // set the params on the ProxyCommand object readCommand.setCommandParams(commandParams); } } if(dataLength > 0) { byte[] commandData = new byte[dataLength]; int dataByteReadIndex = byteReadIndex-dataStartIndex; if(dataStartIndex < byteReadIndex){ System.arraycopy(readBytes, dataStartIndex, commandData, 0, dataByteReadIndex); } while(dataByteReadIndex<dataLength){ if (inputStream.available() < (dataLength-dataByteReadIndex)) { if (!isFramedTransport) { // check if all data bytes have been received. Return immediately for non framed transports return new ProxyCommand(ReadFailure.INSUFFICIENT_DATA, "Stream ended before all data was read. Length of data bytes needed : " + (dataLength-dataByteReadIndex)); } } int actualBytesRead = inputStream.read(commandData, dataByteReadIndex, dataLength-dataByteReadIndex); if (actualBytesRead <= 0) { // 0 bytes not possible because dataLength-dataByteReadIndex is non-zero, -1 is returned if no byte is available because the stream is at end of file (as per Javadocs) throw new IllegalArgumentException("Insufficient bytes read for command : " + readCommand.getCommand() + ". Expected : " + (dataLength-dataByteReadIndex) + " but read : " + actualBytesRead); } dataByteReadIndex += actualBytesRead; } // set the command data on the ProxyCommand object readCommand.setCommandData(commandData); } return readCommand; } /** * Helper method to return the next command fragment position in the input byte array. Considers the start index to skip bytes and the delim char to * identify the next fragment * @return the start position of the next command fragment */ private int getNextCommandFragmentPosition(byte[] arr, int fragmentStart, int lastPos, byte delim) { for(; fragmentStart<lastPos; fragmentStart++) { if(arr[fragmentStart]==delim) { return fragmentStart; } } return fragmentStart; } /** * Helper class to store command protocol objects */ public class ProxyCommand { /** The command String*/ private String command; /** The read failure reason and message*/ private ReadFailure readFailure; private String readFailureDescription; /** The command parameters*/ private Map<String, Object> commandParams = new HashMap<>(); /** The command data*/ private byte[] commandData; /** * Constructor for this class * @param command the command string */ public ProxyCommand(String command) { this.command = command; } /** * Constructor for this class * @param readFailure the ReadFailure reason * @param readFailureDescription the error description */ public ProxyCommand(ReadFailure readFailure, String readFailureDescription) { this.readFailure = readFailure; this.readFailureDescription = readFailureDescription; } /** * Overriden super class method. Returns a string representation of this ProxyCommand * @see java.lang.Object#toString() */ public String toString() { try { return String.format("ProxyCommand[Command = %s, Read Error = %s, Params = %s" + "]",this.getCommand(), this.getReadFailureDescription(), commandParams != null ? OBJECT_MAPPER.writeValueAsString(this.getCommandParams()) : ""); } catch (Exception e) { // ignore JSON formating errors and return just the command string return "ProxyCommand[Command = " + command + ". Read Error = " + readFailureDescription + "]"; } } /** Start setter/getter methods*/ public String getCommand() { return command; } public ReadFailure getReadFailure() { return readFailure; } public String getReadFailureDescription() { return readFailureDescription; } public Map<String, Object> getCommandParams() { return commandParams; } public void setCommandParams(Map<String, Object> commandParams) { this.commandParams = commandParams; } public byte[] getCommandData() { return this.commandData; } public void setCommandData(byte[] commandData) { this.commandData = commandData; } /** End setter/getter methods*/ } }