blob: a48ae3555324ece9a603e776713fd84a53ed5536 [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.coyote.http11.filters;
import java.io.EOFException;
import java.io.IOException;
import org.apache.coyote.InputBuffer;
import org.apache.coyote.Request;
import org.apache.coyote.http11.Constants;
import org.apache.coyote.http11.InputFilter;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.MimeHeaders;
/**
* Chunked input filter. Parses chunked data according to
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1">http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1</a><br>
*
* @author Remy Maucherat
* @author Filip Hanik
*/
public class ChunkedInputFilter implements InputFilter {
// -------------------------------------------------------------- Constants
protected static final String ENCODING_NAME = "chunked";
protected static final ByteChunk ENCODING = new ByteChunk();
// ----------------------------------------------------- Static Initializer
static {
ENCODING.setBytes(ENCODING_NAME.getBytes(), 0, ENCODING_NAME.length());
}
// ----------------------------------------------------- Instance Variables
/**
* Next buffer in the pipeline.
*/
protected InputBuffer buffer;
/**
* Number of bytes remaining in the current chunk.
*/
protected int remaining = 0;
/**
* Position in the buffer.
*/
protected int pos = 0;
/**
* Last valid byte in the buffer.
*/
protected int lastValid = 0;
/**
* Read bytes buffer.
*/
protected byte[] buf = null;
/**
* Byte chunk used to read bytes.
*/
protected ByteChunk readChunk = new ByteChunk();
/**
* Flag set to true when the end chunk has been read.
*/
protected boolean endChunk = false;
/**
* Byte chunk used to store trailing headers.
*/
protected ByteChunk trailingHeaders = new ByteChunk();
/**
* Flag set to true if the next call to doRead() must parse a CRLF pair
* before doing anything else.
*/
protected boolean needCRLFParse = false;
/**
* Request being parsed.
*/
private Request request;
// ----------------------------------------------------------- Constructors
public ChunkedInputFilter(int maxTrailerSize) {
this.trailingHeaders.setLimit(maxTrailerSize);
}
// ---------------------------------------------------- InputBuffer Methods
/**
* Read bytes.
*
* @return If the filter does request length control, this value is
* significant; it should be the number of bytes consumed from the buffer,
* up until the end of the current request body, or the buffer length,
* whichever is greater. If the filter does not do request body length
* control, the returned value should be -1.
*/
@Override
public int doRead(ByteChunk chunk, Request req)
throws IOException {
if (endChunk)
return -1;
if(needCRLFParse) {
needCRLFParse = false;
parseCRLF();
}
if (remaining <= 0) {
if (!parseChunkHeader()) {
throw new IOException("Invalid chunk header");
}
if (endChunk) {
parseEndChunk();
return -1;
}
}
int result = 0;
if (pos >= lastValid) {
readBytes();
}
if (remaining > (lastValid - pos)) {
result = lastValid - pos;
remaining = remaining - result;
chunk.setBytes(buf, pos, result);
pos = lastValid;
} else {
result = remaining;
chunk.setBytes(buf, pos, remaining);
pos = pos + remaining;
remaining = 0;
//we need a CRLF
if ((pos+1) >= lastValid) {
//if we call parseCRLF we overrun the buffer here
//so we defer it to the next call BZ 11117
needCRLFParse = true;
} else {
parseCRLF(); //parse the CRLF immediately
}
}
return result;
}
// ---------------------------------------------------- InputFilter Methods
/**
* Read the content length from the request.
*/
@Override
public void setRequest(Request request) {
this.request = request;
}
/**
* End the current request.
*/
@Override
public long end()
throws IOException {
// Consume extra bytes : parse the stream until the end chunk is found
while (doRead(readChunk, null) >= 0) {
// NOOP: Just consume the input
}
// Return the number of extra bytes which were consumed
return (lastValid - pos);
}
/**
* Amount of bytes still available in a buffer.
*/
@Override
public int available() {
return (lastValid - pos);
}
/**
* Set the next buffer in the filter pipeline.
*/
@Override
public void setBuffer(InputBuffer buffer) {
this.buffer = buffer;
}
/**
* Make the filter ready to process the next request.
*/
@Override
public void recycle() {
remaining = 0;
pos = 0;
lastValid = 0;
endChunk = false;
trailingHeaders.recycle();
}
/**
* Return the name of the associated encoding; Here, the value is
* "identity".
*/
@Override
public ByteChunk getEncodingName() {
return ENCODING;
}
// ------------------------------------------------------ Protected Methods
/**
* Read bytes from the previous buffer.
*/
protected int readBytes()
throws IOException {
int nRead = buffer.doRead(readChunk, null);
pos = readChunk.getStart();
lastValid = pos + nRead;
buf = readChunk.getBytes();
return nRead;
}
/**
* Parse the header of a chunk.
* A chunk header can look like
* A10CRLF
* F23;chunk-extension to be ignoredCRLF
* The letters before CRLF but after the trailer mark, must be valid hex digits,
* we should not parse F23IAMGONNAMESSTHISUP34CRLF as a valid header
* according to spec
*/
protected boolean parseChunkHeader()
throws IOException {
int result = 0;
boolean eol = false;
boolean readDigit = false;
boolean trailer = false;
while (!eol) {
if (pos >= lastValid) {
if (readBytes() <= 0)
return false;
}
if (buf[pos] == Constants.CR) {
// FIXME: Improve parsing to check for CRLF
} else if (buf[pos] == Constants.LF) {
eol = true;
} else if (buf[pos] == Constants.SEMI_COLON) {
trailer = true;
} else if (!trailer) {
//don't read data after the trailer
if (HexUtils.getDec(buf[pos]) != -1) {
readDigit = true;
result *= 16;
result += HexUtils.getDec(buf[pos]);
} else {
//we shouldn't allow invalid, non hex characters
//in the chunked header
return false;
}
}
pos++;
}
if (!readDigit)
return false;
if (result == 0)
endChunk = true;
remaining = result;
if (remaining < 0)
return false;
return true;
}
/**
* Parse CRLF at end of chunk.
*/
protected boolean parseCRLF()
throws IOException {
boolean eol = false;
boolean crfound = false;
while (!eol) {
if (pos >= lastValid) {
if (readBytes() <= 0)
throw new IOException("Invalid CRLF");
}
if (buf[pos] == Constants.CR) {
if (crfound) throw new IOException("Invalid CRLF, two CR characters encountered.");
crfound = true;
} else if (buf[pos] == Constants.LF) {
if (!crfound) throw new IOException("Invalid CRLF, no CR character encountered.");
eol = true;
} else {
throw new IOException("Invalid CRLF");
}
pos++;
}
return true;
}
/**
* Parse end chunk data.
*/
protected void parseEndChunk() throws IOException {
// Handle option trailer headers
while (parseHeader()) {
// Loop until we run out of headers
}
}
private boolean parseHeader() throws IOException {
MimeHeaders headers = request.getMimeHeaders();
byte chr = 0;
while (true) {
// Read new bytes if needed
if (pos >= lastValid) {
if (readBytes() <0)
throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request");
}
chr = buf[pos];
if ((chr == Constants.CR) || (chr == Constants.LF)) {
if (chr == Constants.LF) {
pos++;
return false;
}
} else {
break;
}
pos++;
}
// Mark the current buffer position
int start = trailingHeaders.getEnd();
//
// Reading the header name
// Header name is always US-ASCII
//
boolean colon = false;
while (!colon) {
// Read new bytes if needed
if (pos >= lastValid) {
if (readBytes() <0)
throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request");
}
chr = buf[pos];
if ((chr >= Constants.A) && (chr <= Constants.Z)) {
chr = (byte) (chr - Constants.LC_OFFSET);
}
if (chr == Constants.COLON) {
colon = true;
} else {
trailingHeaders.append(chr);
}
pos++;
}
MessageBytes headerValue = headers.addValue(trailingHeaders.getBytes(),
start, trailingHeaders.getEnd() - start);
// Mark the current buffer position
start = trailingHeaders.getEnd();
//
// Reading the header value (which can be spanned over multiple lines)
//
boolean eol = false;
boolean validLine = true;
int lastSignificantChar = 0;
while (validLine) {
boolean space = true;
// Skipping spaces
while (space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (readBytes() <0)
throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request");
}
chr = buf[pos];
if ((chr == Constants.SP) || (chr == Constants.HT)) {
pos++;
} else {
space = false;
}
}
// Reading bytes until the end of the line
while (!eol) {
// Read new bytes if needed
if (pos >= lastValid) {
if (readBytes() <0)
throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request");
}
chr = buf[pos];
if (chr == Constants.CR) {
// Skip
} else if (chr == Constants.LF) {
eol = true;
} else if (chr == Constants.SP) {
trailingHeaders.append(chr);
} else {
trailingHeaders.append(chr);
lastSignificantChar = trailingHeaders.getEnd();
}
pos++;
}
// Checking the first character of the new line. If the character
// is a LWS, then it's a multiline header
// Read new bytes if needed
if (pos >= lastValid) {
if (readBytes() <0)
throw new EOFException("Unexpected end of stream whilst reading trailer headers for chunked request");
}
chr = buf[pos];
if ((chr != Constants.SP) && (chr != Constants.HT)) {
validLine = false;
} else {
eol = false;
// Copying one extra space in the buffer (since there must
// be at least one space inserted between the lines)
trailingHeaders.append(chr);
}
}
// Set the header value
headerValue.setBytes(trailingHeaders.getBytes(), start,
lastSignificantChar - start);
return true;
}
}