blob: 09515435941034298f02200a0c06aed7094380e7 [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.asyncweb.common.codec;
import java.nio.charset.CharsetDecoder;
import org.apache.asyncweb.common.HttpResponseStatus;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.statemachine.ConsumeToDynamicTerminatorDecodingState;
import org.apache.mina.filter.codec.statemachine.CrLfDecodingState;
import org.apache.mina.filter.codec.statemachine.DecodingState;
import org.apache.mina.filter.codec.statemachine.DecodingStateMachine;
import org.apache.mina.filter.codec.statemachine.FixedLengthDecodingState;
import org.apache.mina.filter.codec.statemachine.SkippingState;
/**
* A decoder which decodes the body of HTTP Requests having
* a "chunked" transfer-coding.
*
* This decoder does <i>not</i> decode trailing entity-headers - it simply
* discards them. Tomcat currently does the same - so this is probably
* the most stable approach for now.<br/>
* If the need arises to decode them in the future, we simply need to employ a
* <code>HttpHeaderDecoder</code> following the last chunk - yielding
* headers for the encountered trailing entity-headers.<p/>
*
* This decoder decodes the following format:
*
* <pre>
* Chunked-Body = *chunk
* last-chunk
* trailer
* CRLF
* chunk = chunk-size [ chunk-extension ] CRLF
* chunk-data CRLF
* chunk-size = 1*HEX
* last-chunk = 1*("0") [ chunk-extension ] CRLF
* chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
* chunk-ext-name = token
* chunk-ext-val = token | quoted-string
* chunk-data = chunk-size(OCTET)
* trailer = *(entity-header CRLF)
* </pre>
*
* <code>ChunkedBodyDecoder</code> employs a <code>SharedBytesAllocator</code>
* to enable the content of each decoded chunk to contribute to a single
* <code>Bytes</code>. This enables all chunks to be read without requiring
* copying.
*
* @author The Apache MINA Project (dev@mina.apache.org)
* @version $Rev$, $Date$
*/
public abstract class ChunkedBodyDecodingState extends DecodingStateMachine {
private final CharsetDecoder asciiDecoder =
HttpCodecUtils.US_ASCII_CHARSET.newDecoder();
private int lastChunkLength;
private boolean chunkHasExtension;
@Override
protected void destroy() throws Exception {
}
@Override
protected DecodingState init() throws Exception {
chunkHasExtension = false;
return READ_CHUNK_LENGTH;
}
private final DecodingState READ_CHUNK_LENGTH = new ConsumeToDynamicTerminatorDecodingState() {
@Override
protected DecodingState finishDecode(IoBuffer product,
ProtocolDecoderOutput out) throws Exception {
if (!product.hasRemaining()) {
HttpCodecUtils
.throwDecoderException("Expected a chunk length.");
}
String length = product.getString(asciiDecoder).trim();
lastChunkLength = Integer.parseInt(length, 16);
if (chunkHasExtension) {
return SKIP_CHUNK_EXTENSION;
}
return AFTER_SKIP_CHUNK_EXTENSION.decode(IoBuffer
.wrap(new byte[] { '\r' }), out);
}
@Override
protected boolean isTerminator(byte b) {
if (!(b >= '0' && b <= '9' || b >= 'a' && b <= 'f' || b >= 'A'
&& b <= 'F' || b == ' ')) {
if (b == ' ' || b == '\r' || b == ';') {
chunkHasExtension = (b == ';' || b == ' ');
return true;
}
throw new IllegalArgumentException();
}
return false;
}
};
private final DecodingState SKIP_CHUNK_EXTENSION = new SkippingState() {
@Override
protected DecodingState finishDecode(int skippedBytes) throws Exception {
return AFTER_SKIP_CHUNK_EXTENSION;
}
@Override
protected boolean canSkip(byte b) {
return b != '\r';
}
};
private final DecodingState AFTER_SKIP_CHUNK_EXTENSION = new CrLfDecodingState() {
@Override
protected DecodingState finishDecode(boolean foundCRLF,
ProtocolDecoderOutput out) throws Exception {
if (!foundCRLF) {
HttpCodecUtils.throwDecoderException(
"Expected CRLF at the end of chunk extension.",
HttpResponseStatus.BAD_REQUEST);
}
if (lastChunkLength <= 0) {
return FIND_END_OF_TRAILER;
} else {
return new FixedLengthDecodingState(lastChunkLength) {
@Override
protected DecodingState finishDecode(IoBuffer readData,
ProtocolDecoderOutput out) throws Exception {
out.write(readData);
// Reset the state.
lastChunkLength = 0;
return AFTER_CHUNK_DATA;
}
};
}
}
};
private final DecodingState AFTER_CHUNK_DATA = new CrLfDecodingState() {
@Override
protected DecodingState finishDecode(boolean foundCRLF,
ProtocolDecoderOutput out) throws Exception {
if (!foundCRLF) {
HttpCodecUtils.throwDecoderException(
"Expected CRLF after a chunk data.",
HttpResponseStatus.BAD_REQUEST);
}
chunkHasExtension = false;
return READ_CHUNK_LENGTH;
}
};
private final DecodingState FIND_END_OF_TRAILER = new CrLfDecodingState() {
@Override
protected DecodingState finishDecode(boolean foundCRLF,
ProtocolDecoderOutput out) throws Exception {
if (foundCRLF) {
return null; // Finish
} else {
return SKIP_ENTITY_HEADER;
}
}
};
private final DecodingState SKIP_ENTITY_HEADER = new SkippingState() {
@Override
protected boolean canSkip(byte b) {
return b != '\r';
}
@Override
protected DecodingState finishDecode(int skippedBytes) throws Exception {
return AFTER_SKIP_ENTITY_HEADER;
}
};
private final DecodingState AFTER_SKIP_ENTITY_HEADER = new CrLfDecodingState() {
@Override
protected DecodingState finishDecode(boolean foundCRLF,
ProtocolDecoderOutput out) throws Exception {
return FIND_END_OF_TRAILER;
}
};
}