/*******************************************************************************
*Copyright (c) 2009 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
*
* This file is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Please contact Eucalyptus Systems, Inc., 130 Castilian
* Dr., Goleta, CA 93101 USA or visit <http://www.eucalyptus.com/licenses/>
* if you need additional information or have any questions.
*
* This file may incorporate work covered under the following copyright and
* permission notice:
*
*
* JBoss, Home of Professional Open Source
*
* Copyright 2009, Red Hat Middleware LLC, and individual contributors
* by the @author tags. See the COPYRIGHT.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*******************************************************************************
* Modified by: chris grzegorczyk <grze@eucalyptus.com>
*/
package com.eucalyptus.ws.handlers.http;
import java.util.List;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.TooLongFrameException;
import org.jboss.netty.handler.codec.http.DefaultHttpChunk;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpMessage;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.codec.replay.ReplayingDecoder;
import org.mortbay.log.Log;
import com.eucalyptus.context.Contexts;
import com.eucalyptus.http.MappingHttpRequest;
import com.eucalyptus.ws.util.HttpUtils;
public class NioHttpDecoder extends ReplayingDecoder<NioHttpDecoder.State> {
private final int maxInitialLineLength;
private final int maxHeaderSize;
private final int maxChunkSize;
protected volatile HttpMessage message;
private volatile ChannelBuffer content;
private volatile long chunkSize;
private int headerSize;
protected enum State {
SKIP_CONTROL_CHARS,
READ_INITIAL,
READ_HEADER,
READ_VARIABLE_LENGTH_CONTENT,
READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS,
READ_FIXED_LENGTH_CONTENT,
READ_FIXED_LENGTH_CONTENT_AS_CHUNKS,
READ_CHUNK_SIZE,
READ_CHUNKED_CONTENT,
READ_CHUNKED_CONTENT_AS_CHUNKS,
READ_CHUNK_DELIMITER,
READ_CHUNK_FOOTER;
}
public NioHttpDecoder( ) {
this( 4096, 8192, 102400 );
}
protected NioHttpDecoder( int maxInitialLineLength, int maxHeaderSize, int maxChunkSize ) {
super( State.SKIP_CONTROL_CHARS, true );
if ( maxInitialLineLength <= 0 ) { throw new IllegalArgumentException( "maxInitialLineLength must be a positive integer: " + maxInitialLineLength ); }
if ( maxHeaderSize <= 0 ) { throw new IllegalArgumentException( "maxHeaderSize must be a positive integer: " + maxChunkSize ); }
if ( maxChunkSize < 0 ) { throw new IllegalArgumentException( "maxChunkSize must be a positive integer: " + maxChunkSize ); }
this.maxInitialLineLength = maxInitialLineLength;
this.maxHeaderSize = maxHeaderSize;
this.maxChunkSize = maxChunkSize;
}
@Override
protected Object decode( ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state ) throws Exception {
switch ( state ) {
case SKIP_CONTROL_CHARS: {
try {
skipControlCharacters( buffer );
checkpoint( State.READ_INITIAL );
} finally {
checkpoint( );
}
}
case READ_INITIAL: {
String[] initialLine = splitInitialLine( HttpUtils.readLine( buffer, maxInitialLineLength ) );
if ( initialLine.length < 3 ) {
checkpoint( State.SKIP_CONTROL_CHARS );
return null;
}
MappingHttpRequest newMessage = new MappingHttpRequest( HttpVersion.valueOf( initialLine[2] ), HttpMethod.valueOf( initialLine[0] ), initialLine[1] );
Contexts.create( newMessage, ctx.getChannel( ) );
message = newMessage;
checkpoint( State.READ_HEADER );
}
case READ_HEADER: {
State nextState = readHeaders( buffer );
checkpoint( nextState );
if ( nextState == State.READ_CHUNK_SIZE ) {
return message;
} else if ( nextState == State.SKIP_CONTROL_CHARS ) {
message.removeHeader( HttpHeaders.Names.CONTENT_LENGTH );
message.removeHeader( HttpHeaders.Names.TRANSFER_ENCODING );
return message;
} else {
long contentLength = message.getContentLength( -1 );
if ( contentLength == 0 || contentLength == -1 && isDecodingRequest( ) ) {
content = ChannelBuffers.EMPTY_BUFFER;
return reset( );
}
switch ( nextState ) {
case READ_FIXED_LENGTH_CONTENT:
if ( contentLength > maxChunkSize ) {
checkpoint( State.READ_FIXED_LENGTH_CONTENT_AS_CHUNKS );
message.addHeader( HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED );
chunkSize = message.getContentLength( -1 );
return message;
}
break;
case READ_VARIABLE_LENGTH_CONTENT:
if ( buffer.readableBytes( ) > maxChunkSize ) {
checkpoint( State.READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS );
message.addHeader( HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED );
return message;
}
break;
}
}
return null;
}
case READ_VARIABLE_LENGTH_CONTENT: {
if ( content == null ) {
content = ChannelBuffers.dynamicBuffer( channel.getConfig( ).getBufferFactory( ) );
}
// this will cause a replay error until the channel is closed where this
// will read what's left in the buffer
content.writeBytes( buffer.readBytes( buffer.readableBytes( ) ) );
return reset( );
}
case READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS: {
// Keep reading data as a chunk until the end of connection is reached.
int chunkSize = Math.min( maxChunkSize, buffer.readableBytes( ) );
HttpChunk chunk = new DefaultHttpChunk( buffer.readBytes( chunkSize ) );
if ( !buffer.readable( ) ) {
// Reached to the end of the connection.
reset( );
if ( !chunk.isLast( ) ) {
// Append the last chunk.
return new Object[] { chunk, HttpChunk.LAST_CHUNK };
}
}
return chunk;
}
case READ_FIXED_LENGTH_CONTENT: {
// we have a content-length so we just read the correct number of bytes
readFixedLengthContent( buffer );
return reset( );
}
case READ_FIXED_LENGTH_CONTENT_AS_CHUNKS: {
long chunkSize = this.chunkSize;
HttpChunk chunk;
if ( chunkSize > maxChunkSize ) {
chunk = new DefaultHttpChunk( buffer.readBytes( maxChunkSize ) );
chunkSize -= maxChunkSize;
} else {
assert chunkSize <= Integer.MAX_VALUE;
chunk = new DefaultHttpChunk( buffer.readBytes( ( int ) chunkSize ) );
chunkSize = 0;
}
this.chunkSize = chunkSize;
if ( chunkSize == 0 ) {
// Read all content.
reset( );
if ( !chunk.isLast( ) ) {
// Append the last chunk.
return new Object[] { chunk, HttpChunk.LAST_CHUNK };
}
}
return chunk;
}
/**
* everything else after this point takes care of reading chunked
* content. basically, read chunk size,
* read chunk, read and ignore the CRLF and repeat until 0
*/
case READ_CHUNK_SIZE: {
String line = HttpUtils.readLine( buffer, maxInitialLineLength );
int chunkSize = getChunkSize( line );
this.chunkSize = chunkSize;
if ( chunkSize == 0 ) {
checkpoint( State.READ_CHUNK_FOOTER );
return null;
} else if ( chunkSize > maxChunkSize ) {
// A chunk is too large. Split them into multiple chunks again.
checkpoint( State.READ_CHUNKED_CONTENT_AS_CHUNKS );
} else {
checkpoint( State.READ_CHUNKED_CONTENT );
}
}
case READ_CHUNKED_CONTENT: {
assert chunkSize <= Integer.MAX_VALUE;
HttpChunk chunk = new DefaultHttpChunk( buffer.readBytes( ( int ) chunkSize ) );
checkpoint( State.READ_CHUNK_DELIMITER );
return chunk;
}
case READ_CHUNKED_CONTENT_AS_CHUNKS: {
long chunkSize = this.chunkSize;
HttpChunk chunk;
if ( chunkSize > maxChunkSize ) {
chunk = new DefaultHttpChunk( buffer.readBytes( maxChunkSize ) );
chunkSize -= maxChunkSize;
} else {
assert chunkSize <= Integer.MAX_VALUE;
chunk = new DefaultHttpChunk( buffer.readBytes( ( int ) chunkSize ) );
chunkSize = 0;
}
this.chunkSize = chunkSize;
if ( chunkSize == 0 ) {
// Read all content.
checkpoint( State.READ_CHUNK_DELIMITER );
}
if ( !chunk.isLast( ) ) { return chunk; }
}
case READ_CHUNK_DELIMITER: {
for ( ;; ) {
byte next = buffer.readByte( );
if ( next == HttpUtils.CR ) {
if ( buffer.readByte( ) == HttpUtils.LF ) {
checkpoint( State.READ_CHUNK_SIZE );
return null;
}
} else if ( next == HttpUtils.LF ) {
checkpoint( State.READ_CHUNK_SIZE );
return null;
}
}
}
case READ_CHUNK_FOOTER: {
// Skip the footer; does anyone use it?
try {
if ( !skipLine( buffer ) ) {
if ( maxChunkSize == 0 ) {
// Chunked encoding disabled.
return reset( );
} else {
reset( );
// The last chunk, which is empty
return HttpChunk.LAST_CHUNK;
}
}
} finally {
checkpoint( );
}
return null;
}
default: {
throw new Error( "Shouldn't reach here." );
}
}
}
private boolean isDecodingRequest( ) {
return true;
}
protected boolean isContentAlwaysEmpty( HttpMessage msg ) {
if ( msg instanceof HttpResponse ) {
HttpResponse res = ( HttpResponse ) msg;
int code = res.getStatus( ).getCode( );
if ( code < 200 ) { return true; }
switch ( code ) {
case 204:
case 205:
case 304:
return true;
}
}
return false;
}
private Object reset( ) {
HttpMessage message = this.message;
ChannelBuffer content = this.content;
if ( content != null ) {
message.setContent( content );
this.content = null;
}
this.message = null;
checkpoint( State.SKIP_CONTROL_CHARS );
return message;
}
private void skipControlCharacters( ChannelBuffer buffer ) {
for ( ;; ) {
char c = ( char ) buffer.readUnsignedByte( );
if ( !Character.isISOControl( c ) && !Character.isWhitespace( c ) ) {
buffer.readerIndex( buffer.readerIndex( ) - 1 );
break;
}
}
}
private void readFixedLengthContent( ChannelBuffer buffer ) {
long length = message.getContentLength( -1 );
assert length <= Integer.MAX_VALUE;
if ( content == null ) {
content = buffer.readBytes( ( int ) length );
} else {
content.writeBytes( buffer.readBytes( ( int ) length ) );
}
}
private State readHeaders( ChannelBuffer buffer ) throws TooLongFrameException {
headerSize = 0;
final HttpMessage message = this.message;
String line = readHeader( buffer );
String lastHeader = null;
if ( line.length( ) != 0 ) {
// message.clearHeaders( );
do {
char firstChar = line.charAt( 0 );
if ( lastHeader != null && ( firstChar == ' ' || firstChar == '\t' ) ) {
List<String> current = message.getHeaders( lastHeader );
int lastPos = current.size( ) - 1;
String newString = current.get( lastPos ) + line.trim( );
current.set( lastPos, newString );
} else {
String[] header = splitHeader( line );
message.addHeader( header[0], header[1] );
lastHeader = header[0];
}
line = readHeader( buffer );
} while ( line.length( ) != 0 );
}
State nextState;
if ( isContentAlwaysEmpty( message ) ) {
nextState = State.SKIP_CONTROL_CHARS;
} else if ( message.isChunked( ) ) {
nextState = State.READ_CHUNK_SIZE;
} else if ( message.getContentLength( -1 ) >= 0 ) {
nextState = State.READ_FIXED_LENGTH_CONTENT;
} else {
nextState = State.READ_VARIABLE_LENGTH_CONTENT;
}
return nextState;
}
private String readHeader( ChannelBuffer buffer ) throws TooLongFrameException {
StringBuilder sb = new StringBuilder( 64 );
int headerSize = this.headerSize;
loop: for ( ;; ) {
char nextByte = ( char ) buffer.readByte( );
headerSize++;
switch ( nextByte ) {
case HttpUtils.CR:
nextByte = ( char ) buffer.readByte( );
headerSize++;
if ( nextByte == HttpUtils.LF ) {
break loop;
}
break;
case HttpUtils.LF:
break loop;
}
// Abort decoding if the header part is too large.
if ( headerSize >= maxHeaderSize ) { throw new TooLongFrameException( "HTTP header is larger than " + maxHeaderSize + " bytes." );
}
sb.append( nextByte );
}
this.headerSize = headerSize;
return sb.toString( );
}
private int getChunkSize( String hex ) {
Log.info( "Chunk Size Hex to parse:" + hex );
hex = hex.replaceAll( "\\W", "" ).trim( );
for ( int i = 0; i < hex.length( ); i++ ) {
char c = hex.charAt( i );
if ( c == ';' || Character.isWhitespace( c ) || Character.isISOControl( c ) ) {
hex = hex.substring( 0, i );
break;
}
}
Log.info( "Chunk Size in Hex:" + hex );
return Integer.parseInt( hex, 16 );
}
/**
* Returns {@code true} if only if the skipped line was not empty.
* Please note that an empty line is also skipped, while {@code} false is
* returned.
*/
private boolean skipLine( ChannelBuffer buffer ) {
int lineLength = 0;
while ( true ) {
byte nextByte = buffer.readByte( );
if ( nextByte == HttpUtils.CR ) {
nextByte = buffer.readByte( );
if ( nextByte == HttpUtils.LF ) { return lineLength != 0; }
} else if ( nextByte == HttpUtils.LF ) {
return lineLength != 0;
} else if ( !Character.isWhitespace( ( char ) nextByte ) ) {
lineLength++;
}
}
}
private String[] splitInitialLine( String sb ) {
int aStart;
int aEnd;
int bStart;
int bEnd;
int cStart;
int cEnd;
aStart = findNonWhitespace( sb, 0 );
aEnd = findWhitespace( sb, aStart );
bStart = findNonWhitespace( sb, aEnd );
bEnd = findWhitespace( sb, bStart );
cStart = findNonWhitespace( sb, bEnd );
cEnd = findEndOfString( sb );
return new String[] { sb.substring( aStart, aEnd ), sb.substring( bStart, bEnd ), sb.substring( cStart, cEnd ) };
}
private String[] splitHeader( String sb ) {
int nameStart;
int nameEnd;
int colonEnd;
int valueStart;
int valueEnd;
nameStart = findNonWhitespace( sb, 0 );
for ( nameEnd = nameStart; nameEnd < sb.length( ); nameEnd++ ) {
char ch = sb.charAt( nameEnd );
if ( ch == ':' || Character.isWhitespace( ch ) ) {
break;
}
}
for ( colonEnd = nameEnd; colonEnd < sb.length( ); colonEnd++ ) {
if ( sb.charAt( colonEnd ) == ':' ) {
colonEnd++;
break;
}
}
valueStart = findNonWhitespace( sb, colonEnd );
valueEnd = findEndOfString( sb );
valueStart = valueStart > valueEnd ? valueEnd : valueStart;
return new String[] { sb.substring( nameStart, nameEnd ), sb.substring( valueStart, valueEnd ) };
}
private int findNonWhitespace( String sb, int offset ) {
int result;
for ( result = offset; result < sb.length( ); result++ ) {
if ( !Character.isWhitespace( sb.charAt( result ) ) ) {
break;
}
}
return result;
}
private int findWhitespace( String sb, int offset ) {
int result;
for ( result = offset; result < sb.length( ); result++ ) {
if ( Character.isWhitespace( sb.charAt( result ) ) ) {
break;
}
}
return result;
}
private int findEndOfString( String sb ) {
int result;
for ( result = sb.length( ); result > 0; result-- ) {
if ( !Character.isWhitespace( sb.charAt( result - 1 ) ) ) {
break;
}
}
return result;
}
}