1519 lines
60 KiB
Java
1519 lines
60 KiB
Java
/*
|
|
* Copyright 2012 The Netty Project
|
|
*
|
|
* The Netty Project 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 io.netty.handler.codec.http.multipart;
|
|
|
|
import io.netty.buffer.ByteBuf;
|
|
import io.netty.handler.codec.http.HttpConstants;
|
|
import io.netty.handler.codec.http.HttpContent;
|
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
|
import io.netty.handler.codec.http.HttpHeaderValues;
|
|
import io.netty.handler.codec.http.HttpRequest;
|
|
import io.netty.handler.codec.http.LastHttpContent;
|
|
import io.netty.handler.codec.http.QueryStringDecoder;
|
|
import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
|
|
import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.TransferEncodingMechanism;
|
|
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
|
|
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
|
|
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
|
|
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
|
|
import io.netty.util.CharsetUtil;
|
|
import io.netty.util.internal.InternalThreadLocalMap;
|
|
import io.netty.util.internal.StringUtil;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.charset.Charset;
|
|
import java.nio.charset.UnsupportedCharsetException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.TreeMap;
|
|
|
|
import static io.netty.buffer.Unpooled.*;
|
|
import static io.netty.util.internal.ObjectUtil.*;
|
|
|
|
/**
|
|
* This decoder will decode Body and can handle POST BODY.
|
|
*
|
|
* You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
|
|
*
|
|
*/
|
|
public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequestDecoder {
|
|
|
|
/**
|
|
* Factory used to create InterfaceHttpData
|
|
*/
|
|
private final HttpDataFactory factory;
|
|
|
|
/**
|
|
* Request to decode
|
|
*/
|
|
private final HttpRequest request;
|
|
|
|
/**
|
|
* Default charset to use
|
|
*/
|
|
private Charset charset;
|
|
|
|
/**
|
|
* Does the last chunk already received
|
|
*/
|
|
private boolean isLastChunk;
|
|
|
|
/**
|
|
* HttpDatas from Body
|
|
*/
|
|
private final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
|
|
|
|
/**
|
|
* HttpDatas as Map from Body
|
|
*/
|
|
private final Map<String, List<InterfaceHttpData>> bodyMapHttpData = new TreeMap<String, List<InterfaceHttpData>>(
|
|
CaseIgnoringComparator.INSTANCE);
|
|
|
|
/**
|
|
* The current channelBuffer
|
|
*/
|
|
private ByteBuf undecodedChunk;
|
|
|
|
/**
|
|
* Body HttpDatas current position
|
|
*/
|
|
private int bodyListHttpDataRank;
|
|
|
|
/**
|
|
* If multipart, this is the boundary for the global multipart
|
|
*/
|
|
private String multipartDataBoundary;
|
|
|
|
/**
|
|
* If multipart, there could be internal multiparts (mixed) to the global
|
|
* multipart. Only one level is allowed.
|
|
*/
|
|
private String multipartMixedBoundary;
|
|
|
|
/**
|
|
* Current getStatus
|
|
*/
|
|
private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
|
|
|
|
/**
|
|
* Used in Multipart
|
|
*/
|
|
private Map<CharSequence, Attribute> currentFieldAttributes;
|
|
|
|
/**
|
|
* The current FileUpload that is currently in decode process
|
|
*/
|
|
private FileUpload currentFileUpload;
|
|
|
|
/**
|
|
* The current Attribute that is currently in decode process
|
|
*/
|
|
private Attribute currentAttribute;
|
|
|
|
private boolean destroyed;
|
|
|
|
private int discardThreshold = HttpPostRequestDecoder.DEFAULT_DISCARD_THRESHOLD;
|
|
|
|
/**
|
|
*
|
|
* @param request
|
|
* the request to decode
|
|
* @throws NullPointerException
|
|
* for request
|
|
* @throws ErrorDataDecoderException
|
|
* if the default charset was wrong when decoding or other
|
|
* errors
|
|
*/
|
|
public HttpPostMultipartRequestDecoder(HttpRequest request) {
|
|
this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param factory
|
|
* the factory used to create InterfaceHttpData
|
|
* @param request
|
|
* the request to decode
|
|
* @throws NullPointerException
|
|
* for request or factory
|
|
* @throws ErrorDataDecoderException
|
|
* if the default charset was wrong when decoding or other
|
|
* errors
|
|
*/
|
|
public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request) {
|
|
this(factory, request, HttpConstants.DEFAULT_CHARSET);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param factory
|
|
* the factory used to create InterfaceHttpData
|
|
* @param request
|
|
* the request to decode
|
|
* @param charset
|
|
* the charset to use as default
|
|
* @throws NullPointerException
|
|
* for request or charset or factory
|
|
* @throws ErrorDataDecoderException
|
|
* if the default charset was wrong when decoding or other
|
|
* errors
|
|
*/
|
|
public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
|
|
this.request = checkNotNull(request, "request");
|
|
this.charset = checkNotNull(charset, "charset");
|
|
this.factory = checkNotNull(factory, "factory");
|
|
// Fill default values
|
|
|
|
setMultipart(this.request.headers().get(HttpHeaderNames.CONTENT_TYPE));
|
|
if (request instanceof HttpContent) {
|
|
// Offer automatically if the given request is als type of HttpContent
|
|
// See #1089
|
|
offer((HttpContent) request);
|
|
} else {
|
|
undecodedChunk = buffer();
|
|
parseBody();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set from the request ContentType the multipartDataBoundary and the possible charset.
|
|
*/
|
|
private void setMultipart(String contentType) {
|
|
String[] dataBoundary = HttpPostRequestDecoder.getMultipartDataBoundary(contentType);
|
|
if (dataBoundary != null) {
|
|
multipartDataBoundary = dataBoundary[0];
|
|
if (dataBoundary.length > 1 && dataBoundary[1] != null) {
|
|
charset = Charset.forName(dataBoundary[1]);
|
|
}
|
|
} else {
|
|
multipartDataBoundary = null;
|
|
}
|
|
currentStatus = MultiPartStatus.HEADERDELIMITER;
|
|
}
|
|
|
|
private void checkDestroyed() {
|
|
if (destroyed) {
|
|
throw new IllegalStateException(HttpPostMultipartRequestDecoder.class.getSimpleName()
|
|
+ " was destroyed already");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* True if this request is a Multipart request
|
|
*
|
|
* @return True if this request is a Multipart request
|
|
*/
|
|
@Override
|
|
public boolean isMultipart() {
|
|
checkDestroyed();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set the amount of bytes after which read bytes in the buffer should be discarded.
|
|
* Setting this lower gives lower memory usage but with the overhead of more memory copies.
|
|
* Use {@code 0} to disable it.
|
|
*/
|
|
@Override
|
|
public void setDiscardThreshold(int discardThreshold) {
|
|
this.discardThreshold = checkPositiveOrZero(discardThreshold, "discardThreshold");
|
|
}
|
|
|
|
/**
|
|
* Return the threshold in bytes after which read data in the buffer should be discarded.
|
|
*/
|
|
@Override
|
|
public int getDiscardThreshold() {
|
|
return discardThreshold;
|
|
}
|
|
|
|
/**
|
|
* This getMethod returns a List of all HttpDatas from body.<br>
|
|
*
|
|
* If chunked, all chunks must have been offered using offer() getMethod. If
|
|
* not, NotEnoughDataDecoderException will be raised.
|
|
*
|
|
* @return the list of HttpDatas from Body part for POST getMethod
|
|
* @throws NotEnoughDataDecoderException
|
|
* Need more chunks
|
|
*/
|
|
@Override
|
|
public List<InterfaceHttpData> getBodyHttpDatas() {
|
|
checkDestroyed();
|
|
|
|
if (!isLastChunk) {
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
return bodyListHttpData;
|
|
}
|
|
|
|
/**
|
|
* This getMethod returns a List of all HttpDatas with the given name from
|
|
* body.<br>
|
|
*
|
|
* If chunked, all chunks must have been offered using offer() getMethod. If
|
|
* not, NotEnoughDataDecoderException will be raised.
|
|
*
|
|
* @return All Body HttpDatas with the given name (ignore case)
|
|
* @throws NotEnoughDataDecoderException
|
|
* need more chunks
|
|
*/
|
|
@Override
|
|
public List<InterfaceHttpData> getBodyHttpDatas(String name) {
|
|
checkDestroyed();
|
|
|
|
if (!isLastChunk) {
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
return bodyMapHttpData.get(name);
|
|
}
|
|
|
|
/**
|
|
* This getMethod returns the first InterfaceHttpData with the given name from
|
|
* body.<br>
|
|
*
|
|
* If chunked, all chunks must have been offered using offer() getMethod. If
|
|
* not, NotEnoughDataDecoderException will be raised.
|
|
*
|
|
* @return The first Body InterfaceHttpData with the given name (ignore
|
|
* case)
|
|
* @throws NotEnoughDataDecoderException
|
|
* need more chunks
|
|
*/
|
|
@Override
|
|
public InterfaceHttpData getBodyHttpData(String name) {
|
|
checkDestroyed();
|
|
|
|
if (!isLastChunk) {
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
List<InterfaceHttpData> list = bodyMapHttpData.get(name);
|
|
if (list != null) {
|
|
return list.get(0);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Initialized the internals from a new chunk
|
|
*
|
|
* @param content
|
|
* the new received chunk
|
|
* @throws ErrorDataDecoderException
|
|
* if there is a problem with the charset decoding or other
|
|
* errors
|
|
*/
|
|
@Override
|
|
public HttpPostMultipartRequestDecoder offer(HttpContent content) {
|
|
checkDestroyed();
|
|
|
|
// Maybe we should better not copy here for performance reasons but this will need
|
|
// more care by the caller to release the content in a correct manner later
|
|
// So maybe something to optimize on a later stage
|
|
ByteBuf buf = content.content();
|
|
if (undecodedChunk == null) {
|
|
undecodedChunk = buf.copy();
|
|
} else {
|
|
undecodedChunk.writeBytes(buf);
|
|
}
|
|
if (content instanceof LastHttpContent) {
|
|
isLastChunk = true;
|
|
}
|
|
parseBody();
|
|
if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
|
|
undecodedChunk.discardReadBytes();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* True if at current getStatus, there is an available decoded
|
|
* InterfaceHttpData from the Body.
|
|
*
|
|
* This getMethod works for chunked and not chunked request.
|
|
*
|
|
* @return True if at current getStatus, there is a decoded InterfaceHttpData
|
|
* @throws EndOfDataDecoderException
|
|
* No more data will be available
|
|
*/
|
|
@Override
|
|
public boolean hasNext() {
|
|
checkDestroyed();
|
|
|
|
if (currentStatus == MultiPartStatus.EPILOGUE) {
|
|
// OK except if end of list
|
|
if (bodyListHttpDataRank >= bodyListHttpData.size()) {
|
|
throw new EndOfDataDecoderException();
|
|
}
|
|
}
|
|
return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size();
|
|
}
|
|
|
|
/**
|
|
* Returns the next available InterfaceHttpData or null if, at the time it
|
|
* is called, there is no more available InterfaceHttpData. A subsequent
|
|
* call to offer(httpChunk) could enable more data.
|
|
*
|
|
* Be sure to call {@link InterfaceHttpData#release()} after you are done
|
|
* with processing to make sure to not leak any resources
|
|
*
|
|
* @return the next available InterfaceHttpData or null if none
|
|
* @throws EndOfDataDecoderException
|
|
* No more data will be available
|
|
*/
|
|
@Override
|
|
public InterfaceHttpData next() {
|
|
checkDestroyed();
|
|
|
|
if (hasNext()) {
|
|
return bodyListHttpData.get(bodyListHttpDataRank++);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public InterfaceHttpData currentPartialHttpData() {
|
|
if (currentFileUpload != null) {
|
|
return currentFileUpload;
|
|
} else {
|
|
return currentAttribute;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This getMethod will parse as much as possible data and fill the list and map
|
|
*
|
|
* @throws ErrorDataDecoderException
|
|
* if there is a problem with the charset decoding or other
|
|
* errors
|
|
*/
|
|
private void parseBody() {
|
|
if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
|
|
if (isLastChunk) {
|
|
currentStatus = MultiPartStatus.EPILOGUE;
|
|
}
|
|
return;
|
|
}
|
|
parseBodyMultipart();
|
|
}
|
|
|
|
/**
|
|
* Utility function to add a new decoded data
|
|
*/
|
|
protected void addHttpData(InterfaceHttpData data) {
|
|
if (data == null) {
|
|
return;
|
|
}
|
|
List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
|
|
if (datas == null) {
|
|
datas = new ArrayList<InterfaceHttpData>(1);
|
|
bodyMapHttpData.put(data.getName(), datas);
|
|
}
|
|
datas.add(data);
|
|
bodyListHttpData.add(data);
|
|
}
|
|
|
|
/**
|
|
* Parse the Body for multipart
|
|
*
|
|
* @throws ErrorDataDecoderException
|
|
* if there is a problem with the charset decoding or other
|
|
* errors
|
|
*/
|
|
private void parseBodyMultipart() {
|
|
if (undecodedChunk == null || undecodedChunk.readableBytes() == 0) {
|
|
// nothing to decode
|
|
return;
|
|
}
|
|
InterfaceHttpData data = decodeMultipart(currentStatus);
|
|
while (data != null) {
|
|
addHttpData(data);
|
|
if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
|
|
break;
|
|
}
|
|
data = decodeMultipart(currentStatus);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode a multipart request by pieces<br>
|
|
* <br>
|
|
* NOTSTARTED PREAMBLE (<br>
|
|
* (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*<br>
|
|
* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE<br>
|
|
* (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+<br>
|
|
* MIXEDCLOSEDELIMITER)*<br>
|
|
* CLOSEDELIMITER)+ EPILOGUE<br>
|
|
*
|
|
* Inspired from HttpMessageDecoder
|
|
*
|
|
* @return the next decoded InterfaceHttpData or null if none until now.
|
|
* @throws ErrorDataDecoderException
|
|
* if an error occurs
|
|
*/
|
|
private InterfaceHttpData decodeMultipart(MultiPartStatus state) {
|
|
switch (state) {
|
|
case NOTSTARTED:
|
|
throw new ErrorDataDecoderException("Should not be called with the current getStatus");
|
|
case PREAMBLE:
|
|
// Content-type: multipart/form-data, boundary=AaB03x
|
|
throw new ErrorDataDecoderException("Should not be called with the current getStatus");
|
|
case HEADERDELIMITER: {
|
|
// --AaB03x or --AaB03x--
|
|
return findMultipartDelimiter(multipartDataBoundary, MultiPartStatus.DISPOSITION,
|
|
MultiPartStatus.PREEPILOGUE);
|
|
}
|
|
case DISPOSITION: {
|
|
// content-disposition: form-data; name="field1"
|
|
// content-disposition: form-data; name="pics"; filename="file1.txt"
|
|
// and other immediate values like
|
|
// Content-type: image/gif
|
|
// Content-Type: text/plain
|
|
// Content-Type: text/plain; charset=ISO-8859-1
|
|
// Content-Transfer-Encoding: binary
|
|
// The following line implies a change of mode (mixed mode)
|
|
// Content-type: multipart/mixed, boundary=BbC04y
|
|
return findMultipartDisposition();
|
|
}
|
|
case FIELD: {
|
|
// Now get value according to Content-Type and Charset
|
|
Charset localCharset = null;
|
|
Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaderValues.CHARSET);
|
|
if (charsetAttribute != null) {
|
|
try {
|
|
localCharset = Charset.forName(charsetAttribute.getValue());
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (UnsupportedCharsetException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
}
|
|
Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
|
|
if (currentAttribute == null) {
|
|
Attribute lengthAttribute = currentFieldAttributes
|
|
.get(HttpHeaderNames.CONTENT_LENGTH);
|
|
long size;
|
|
try {
|
|
size = lengthAttribute != null? Long.parseLong(lengthAttribute
|
|
.getValue()) : 0L;
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (NumberFormatException ignored) {
|
|
size = 0;
|
|
}
|
|
try {
|
|
if (size > 0) {
|
|
currentAttribute = factory.createAttribute(request,
|
|
cleanString(nameAttribute.getValue()), size);
|
|
} else {
|
|
currentAttribute = factory.createAttribute(request,
|
|
cleanString(nameAttribute.getValue()));
|
|
}
|
|
} catch (NullPointerException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
if (localCharset != null) {
|
|
currentAttribute.setCharset(localCharset);
|
|
}
|
|
}
|
|
// load data
|
|
if (!loadDataMultipart(undecodedChunk, multipartDataBoundary, currentAttribute)) {
|
|
// Delimiter is not found. Need more chunks.
|
|
return null;
|
|
}
|
|
Attribute finalAttribute = currentAttribute;
|
|
currentAttribute = null;
|
|
currentFieldAttributes = null;
|
|
// ready to load the next one
|
|
currentStatus = MultiPartStatus.HEADERDELIMITER;
|
|
return finalAttribute;
|
|
}
|
|
case FILEUPLOAD: {
|
|
// eventually restart from existing FileUpload
|
|
return getFileUpload(multipartDataBoundary);
|
|
}
|
|
case MIXEDDELIMITER: {
|
|
// --AaB03x or --AaB03x--
|
|
// Note that currentFieldAttributes exists
|
|
return findMultipartDelimiter(multipartMixedBoundary, MultiPartStatus.MIXEDDISPOSITION,
|
|
MultiPartStatus.HEADERDELIMITER);
|
|
}
|
|
case MIXEDDISPOSITION: {
|
|
return findMultipartDisposition();
|
|
}
|
|
case MIXEDFILEUPLOAD: {
|
|
// eventually restart from existing FileUpload
|
|
return getFileUpload(multipartMixedBoundary);
|
|
}
|
|
case PREEPILOGUE:
|
|
return null;
|
|
case EPILOGUE:
|
|
return null;
|
|
default:
|
|
throw new ErrorDataDecoderException("Shouldn't reach here.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skip control Characters
|
|
*
|
|
* @throws NotEnoughDataDecoderException
|
|
*/
|
|
private static void skipControlCharacters(ByteBuf undecodedChunk) {
|
|
if (!undecodedChunk.hasArray()) {
|
|
try {
|
|
skipControlCharactersStandard(undecodedChunk);
|
|
} catch (IndexOutOfBoundsException e1) {
|
|
throw new NotEnoughDataDecoderException(e1);
|
|
}
|
|
return;
|
|
}
|
|
SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
|
|
while (sao.pos < sao.limit) {
|
|
char c = (char) (sao.bytes[sao.pos++] & 0xFF);
|
|
if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
|
|
sao.setReadPosition(1);
|
|
return;
|
|
}
|
|
}
|
|
throw new NotEnoughDataDecoderException("Access out of bounds");
|
|
}
|
|
|
|
private static void skipControlCharactersStandard(ByteBuf undecodedChunk) {
|
|
for (;;) {
|
|
char c = (char) undecodedChunk.readUnsignedByte();
|
|
if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
|
|
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the next Multipart Delimiter
|
|
*
|
|
* @param delimiter
|
|
* delimiter to find
|
|
* @param dispositionStatus
|
|
* the next getStatus if the delimiter is a start
|
|
* @param closeDelimiterStatus
|
|
* the next getStatus if the delimiter is a close delimiter
|
|
* @return the next InterfaceHttpData if any
|
|
* @throws ErrorDataDecoderException
|
|
*/
|
|
private InterfaceHttpData findMultipartDelimiter(String delimiter, MultiPartStatus dispositionStatus,
|
|
MultiPartStatus closeDelimiterStatus) {
|
|
// --AaB03x or --AaB03x--
|
|
int readerIndex = undecodedChunk.readerIndex();
|
|
try {
|
|
skipControlCharacters(undecodedChunk);
|
|
} catch (NotEnoughDataDecoderException ignored) {
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
return null;
|
|
}
|
|
skipOneLine();
|
|
String newline;
|
|
try {
|
|
newline = readDelimiter(undecodedChunk, delimiter);
|
|
} catch (NotEnoughDataDecoderException ignored) {
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
return null;
|
|
}
|
|
if (newline.equals(delimiter)) {
|
|
currentStatus = dispositionStatus;
|
|
return decodeMultipart(dispositionStatus);
|
|
}
|
|
if (newline.equals(delimiter + "--")) {
|
|
// CLOSEDELIMITER or MIXED CLOSEDELIMITER found
|
|
currentStatus = closeDelimiterStatus;
|
|
if (currentStatus == MultiPartStatus.HEADERDELIMITER) {
|
|
// MIXEDCLOSEDELIMITER
|
|
// end of the Mixed part
|
|
currentFieldAttributes = null;
|
|
return decodeMultipart(MultiPartStatus.HEADERDELIMITER);
|
|
}
|
|
return null;
|
|
}
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new ErrorDataDecoderException("No Multipart delimiter found");
|
|
}
|
|
|
|
/**
|
|
* Find the next Disposition
|
|
*
|
|
* @return the next InterfaceHttpData if any
|
|
* @throws ErrorDataDecoderException
|
|
*/
|
|
private InterfaceHttpData findMultipartDisposition() {
|
|
int readerIndex = undecodedChunk.readerIndex();
|
|
if (currentStatus == MultiPartStatus.DISPOSITION) {
|
|
currentFieldAttributes = new TreeMap<CharSequence, Attribute>(CaseIgnoringComparator.INSTANCE);
|
|
}
|
|
// read many lines until empty line with newline found! Store all data
|
|
while (!skipOneLine()) {
|
|
String newline;
|
|
try {
|
|
skipControlCharacters(undecodedChunk);
|
|
newline = readLine(undecodedChunk, charset);
|
|
} catch (NotEnoughDataDecoderException ignored) {
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
return null;
|
|
}
|
|
String[] contents = splitMultipartHeader(newline);
|
|
if (HttpHeaderNames.CONTENT_DISPOSITION.contentEqualsIgnoreCase(contents[0])) {
|
|
boolean checkSecondArg;
|
|
if (currentStatus == MultiPartStatus.DISPOSITION) {
|
|
checkSecondArg = HttpHeaderValues.FORM_DATA.contentEqualsIgnoreCase(contents[1]);
|
|
} else {
|
|
checkSecondArg = HttpHeaderValues.ATTACHMENT.contentEqualsIgnoreCase(contents[1])
|
|
|| HttpHeaderValues.FILE.contentEqualsIgnoreCase(contents[1]);
|
|
}
|
|
if (checkSecondArg) {
|
|
// read next values and store them in the map as Attribute
|
|
for (int i = 2; i < contents.length; i++) {
|
|
String[] values = contents[i].split("=", 2);
|
|
Attribute attribute;
|
|
try {
|
|
attribute = getContentDispositionAttribute(values);
|
|
} catch (NullPointerException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
currentFieldAttributes.put(attribute.getName(), attribute);
|
|
}
|
|
}
|
|
} else if (HttpHeaderNames.CONTENT_TRANSFER_ENCODING.contentEqualsIgnoreCase(contents[0])) {
|
|
Attribute attribute;
|
|
try {
|
|
attribute = factory.createAttribute(request, HttpHeaderNames.CONTENT_TRANSFER_ENCODING.toString(),
|
|
cleanString(contents[1]));
|
|
} catch (NullPointerException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
|
|
currentFieldAttributes.put(HttpHeaderNames.CONTENT_TRANSFER_ENCODING, attribute);
|
|
} else if (HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(contents[0])) {
|
|
Attribute attribute;
|
|
try {
|
|
attribute = factory.createAttribute(request, HttpHeaderNames.CONTENT_LENGTH.toString(),
|
|
cleanString(contents[1]));
|
|
} catch (NullPointerException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
|
|
currentFieldAttributes.put(HttpHeaderNames.CONTENT_LENGTH, attribute);
|
|
} else if (HttpHeaderNames.CONTENT_TYPE.contentEqualsIgnoreCase(contents[0])) {
|
|
// Take care of possible "multipart/mixed"
|
|
if (HttpHeaderValues.MULTIPART_MIXED.contentEqualsIgnoreCase(contents[1])) {
|
|
if (currentStatus == MultiPartStatus.DISPOSITION) {
|
|
String values = StringUtil.substringAfter(contents[2], '=');
|
|
multipartMixedBoundary = "--" + values;
|
|
currentStatus = MultiPartStatus.MIXEDDELIMITER;
|
|
return decodeMultipart(MultiPartStatus.MIXEDDELIMITER);
|
|
} else {
|
|
throw new ErrorDataDecoderException("Mixed Multipart found in a previous Mixed Multipart");
|
|
}
|
|
} else {
|
|
for (int i = 1; i < contents.length; i++) {
|
|
final String charsetHeader = HttpHeaderValues.CHARSET.toString();
|
|
if (contents[i].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {
|
|
String values = StringUtil.substringAfter(contents[i], '=');
|
|
Attribute attribute;
|
|
try {
|
|
attribute = factory.createAttribute(request, charsetHeader, cleanString(values));
|
|
} catch (NullPointerException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
currentFieldAttributes.put(HttpHeaderValues.CHARSET, attribute);
|
|
} else {
|
|
Attribute attribute;
|
|
try {
|
|
attribute = factory.createAttribute(request,
|
|
cleanString(contents[0]), contents[i]);
|
|
} catch (NullPointerException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
currentFieldAttributes.put(attribute.getName(), attribute);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Is it a FileUpload
|
|
Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
|
|
if (currentStatus == MultiPartStatus.DISPOSITION) {
|
|
if (filenameAttribute != null) {
|
|
// FileUpload
|
|
currentStatus = MultiPartStatus.FILEUPLOAD;
|
|
// do not change the buffer position
|
|
return decodeMultipart(MultiPartStatus.FILEUPLOAD);
|
|
} else {
|
|
// Field
|
|
currentStatus = MultiPartStatus.FIELD;
|
|
// do not change the buffer position
|
|
return decodeMultipart(MultiPartStatus.FIELD);
|
|
}
|
|
} else {
|
|
if (filenameAttribute != null) {
|
|
// FileUpload
|
|
currentStatus = MultiPartStatus.MIXEDFILEUPLOAD;
|
|
// do not change the buffer position
|
|
return decodeMultipart(MultiPartStatus.MIXEDFILEUPLOAD);
|
|
} else {
|
|
// Field is not supported in MIXED mode
|
|
throw new ErrorDataDecoderException("Filename not found");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final String FILENAME_ENCODED = HttpHeaderValues.FILENAME.toString() + '*';
|
|
|
|
private Attribute getContentDispositionAttribute(String... values) {
|
|
String name = cleanString(values[0]);
|
|
String value = values[1];
|
|
|
|
// Filename can be token, quoted or encoded. See https://tools.ietf.org/html/rfc5987
|
|
if (HttpHeaderValues.FILENAME.contentEquals(name)) {
|
|
// Value is quoted or token. Strip if quoted:
|
|
int last = value.length() - 1;
|
|
if (last > 0 &&
|
|
value.charAt(0) == HttpConstants.DOUBLE_QUOTE &&
|
|
value.charAt(last) == HttpConstants.DOUBLE_QUOTE) {
|
|
value = value.substring(1, last);
|
|
}
|
|
} else if (FILENAME_ENCODED.equals(name)) {
|
|
try {
|
|
name = HttpHeaderValues.FILENAME.toString();
|
|
String[] split = value.split("'", 3);
|
|
value = QueryStringDecoder.decodeComponent(split[2], Charset.forName(split[0]));
|
|
} catch (ArrayIndexOutOfBoundsException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (UnsupportedCharsetException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
} else {
|
|
// otherwise we need to clean the value
|
|
value = cleanString(value);
|
|
}
|
|
return factory.createAttribute(request, name, value);
|
|
}
|
|
|
|
/**
|
|
* Get the FileUpload (new one or current one)
|
|
*
|
|
* @param delimiter
|
|
* the delimiter to use
|
|
* @return the InterfaceHttpData if any
|
|
* @throws ErrorDataDecoderException
|
|
*/
|
|
protected InterfaceHttpData getFileUpload(String delimiter) {
|
|
// eventually restart from existing FileUpload
|
|
// Now get value according to Content-Type and Charset
|
|
Attribute encoding = currentFieldAttributes.get(HttpHeaderNames.CONTENT_TRANSFER_ENCODING);
|
|
Charset localCharset = charset;
|
|
// Default
|
|
TransferEncodingMechanism mechanism = TransferEncodingMechanism.BIT7;
|
|
if (encoding != null) {
|
|
String code;
|
|
try {
|
|
code = encoding.getValue().toLowerCase();
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT7.value())) {
|
|
localCharset = CharsetUtil.US_ASCII;
|
|
} else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT8.value())) {
|
|
localCharset = CharsetUtil.ISO_8859_1;
|
|
mechanism = TransferEncodingMechanism.BIT8;
|
|
} else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
|
|
// no real charset, so let the default
|
|
mechanism = TransferEncodingMechanism.BINARY;
|
|
} else {
|
|
throw new ErrorDataDecoderException("TransferEncoding Unknown: " + code);
|
|
}
|
|
}
|
|
Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaderValues.CHARSET);
|
|
if (charsetAttribute != null) {
|
|
try {
|
|
localCharset = Charset.forName(charsetAttribute.getValue());
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (UnsupportedCharsetException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
}
|
|
if (currentFileUpload == null) {
|
|
Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
|
|
Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
|
|
Attribute contentTypeAttribute = currentFieldAttributes.get(HttpHeaderNames.CONTENT_TYPE);
|
|
Attribute lengthAttribute = currentFieldAttributes.get(HttpHeaderNames.CONTENT_LENGTH);
|
|
long size;
|
|
try {
|
|
size = lengthAttribute != null ? Long.parseLong(lengthAttribute.getValue()) : 0L;
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (NumberFormatException ignored) {
|
|
size = 0;
|
|
}
|
|
try {
|
|
String contentType;
|
|
if (contentTypeAttribute != null) {
|
|
contentType = contentTypeAttribute.getValue();
|
|
} else {
|
|
contentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
|
|
}
|
|
currentFileUpload = factory.createFileUpload(request,
|
|
cleanString(nameAttribute.getValue()), cleanString(filenameAttribute.getValue()),
|
|
contentType, mechanism.value(), localCharset,
|
|
size);
|
|
} catch (NullPointerException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
}
|
|
// load data as much as possible
|
|
if (!loadDataMultipart(undecodedChunk, delimiter, currentFileUpload)) {
|
|
// Delimiter is not found. Need more chunks.
|
|
return null;
|
|
}
|
|
if (currentFileUpload.isCompleted()) {
|
|
// ready to load the next one
|
|
if (currentStatus == MultiPartStatus.FILEUPLOAD) {
|
|
currentStatus = MultiPartStatus.HEADERDELIMITER;
|
|
currentFieldAttributes = null;
|
|
} else {
|
|
currentStatus = MultiPartStatus.MIXEDDELIMITER;
|
|
cleanMixedAttributes();
|
|
}
|
|
FileUpload fileUpload = currentFileUpload;
|
|
currentFileUpload = null;
|
|
return fileUpload;
|
|
}
|
|
// do not change the buffer position
|
|
// since some can be already saved into FileUpload
|
|
// So do not change the currentStatus
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Destroy the {@link HttpPostMultipartRequestDecoder} and release all it resources. After this method
|
|
* was called it is not possible to operate on it anymore.
|
|
*/
|
|
@Override
|
|
public void destroy() {
|
|
checkDestroyed();
|
|
cleanFiles();
|
|
destroyed = true;
|
|
|
|
if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
|
|
undecodedChunk.release();
|
|
undecodedChunk = null;
|
|
}
|
|
|
|
// release all data which was not yet pulled
|
|
for (int i = bodyListHttpDataRank; i < bodyListHttpData.size(); i++) {
|
|
bodyListHttpData.get(i).release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean all HttpDatas (on Disk) for the current request.
|
|
*/
|
|
@Override
|
|
public void cleanFiles() {
|
|
checkDestroyed();
|
|
|
|
factory.cleanRequestHttpData(request);
|
|
}
|
|
|
|
/**
|
|
* Remove the given FileUpload from the list of FileUploads to clean
|
|
*/
|
|
@Override
|
|
public void removeHttpDataFromClean(InterfaceHttpData data) {
|
|
checkDestroyed();
|
|
|
|
factory.removeHttpDataFromClean(request, data);
|
|
}
|
|
|
|
/**
|
|
* Remove all Attributes that should be cleaned between two FileUpload in
|
|
* Mixed mode
|
|
*/
|
|
private void cleanMixedAttributes() {
|
|
currentFieldAttributes.remove(HttpHeaderValues.CHARSET);
|
|
currentFieldAttributes.remove(HttpHeaderNames.CONTENT_LENGTH);
|
|
currentFieldAttributes.remove(HttpHeaderNames.CONTENT_TRANSFER_ENCODING);
|
|
currentFieldAttributes.remove(HttpHeaderNames.CONTENT_TYPE);
|
|
currentFieldAttributes.remove(HttpHeaderValues.FILENAME);
|
|
}
|
|
|
|
/**
|
|
* Read one line up to the CRLF or LF
|
|
*
|
|
* @return the String from one line
|
|
* @throws NotEnoughDataDecoderException
|
|
* Need more chunks and reset the {@code readerIndex} to the previous
|
|
* value
|
|
*/
|
|
private static String readLineStandard(ByteBuf undecodedChunk, Charset charset) {
|
|
int readerIndex = undecodedChunk.readerIndex();
|
|
try {
|
|
ByteBuf line = buffer(64);
|
|
|
|
while (undecodedChunk.isReadable()) {
|
|
byte nextByte = undecodedChunk.readByte();
|
|
if (nextByte == HttpConstants.CR) {
|
|
// check but do not changed readerIndex
|
|
nextByte = undecodedChunk.getByte(undecodedChunk.readerIndex());
|
|
if (nextByte == HttpConstants.LF) {
|
|
// force read
|
|
undecodedChunk.readByte();
|
|
return line.toString(charset);
|
|
} else {
|
|
// Write CR (not followed by LF)
|
|
line.writeByte(HttpConstants.CR);
|
|
}
|
|
} else if (nextByte == HttpConstants.LF) {
|
|
return line.toString(charset);
|
|
} else {
|
|
line.writeByte(nextByte);
|
|
}
|
|
}
|
|
} catch (IndexOutOfBoundsException e) {
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException(e);
|
|
}
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
|
|
/**
|
|
* Read one line up to the CRLF or LF
|
|
*
|
|
* @return the String from one line
|
|
* @throws NotEnoughDataDecoderException
|
|
* Need more chunks and reset the {@code readerIndex} to the previous
|
|
* value
|
|
*/
|
|
private static String readLine(ByteBuf undecodedChunk, Charset charset) {
|
|
if (!undecodedChunk.hasArray()) {
|
|
return readLineStandard(undecodedChunk, charset);
|
|
}
|
|
SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
|
|
int readerIndex = undecodedChunk.readerIndex();
|
|
try {
|
|
ByteBuf line = buffer(64);
|
|
|
|
while (sao.pos < sao.limit) {
|
|
byte nextByte = sao.bytes[sao.pos++];
|
|
if (nextByte == HttpConstants.CR) {
|
|
if (sao.pos < sao.limit) {
|
|
nextByte = sao.bytes[sao.pos++];
|
|
if (nextByte == HttpConstants.LF) {
|
|
sao.setReadPosition(0);
|
|
return line.toString(charset);
|
|
} else {
|
|
// Write CR (not followed by LF)
|
|
sao.pos--;
|
|
line.writeByte(HttpConstants.CR);
|
|
}
|
|
} else {
|
|
line.writeByte(nextByte);
|
|
}
|
|
} else if (nextByte == HttpConstants.LF) {
|
|
sao.setReadPosition(0);
|
|
return line.toString(charset);
|
|
} else {
|
|
line.writeByte(nextByte);
|
|
}
|
|
}
|
|
} catch (IndexOutOfBoundsException e) {
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException(e);
|
|
}
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
|
|
/**
|
|
* Read one line up to --delimiter or --delimiter-- and if existing the CRLF
|
|
* or LF Read one line up to --delimiter or --delimiter-- and if existing
|
|
* the CRLF or LF. Note that CRLF or LF are mandatory for opening delimiter
|
|
* (--delimiter) but not for closing delimiter (--delimiter--) since some
|
|
* clients does not include CRLF in this case.
|
|
*
|
|
* @param delimiter
|
|
* of the form --string, such that '--' is already included
|
|
* @return the String from one line as the delimiter searched (opening or
|
|
* closing)
|
|
* @throws NotEnoughDataDecoderException
|
|
* Need more chunks and reset the {@code readerIndex} to the previous
|
|
* value
|
|
*/
|
|
private static String readDelimiterStandard(ByteBuf undecodedChunk, String delimiter) {
|
|
int readerIndex = undecodedChunk.readerIndex();
|
|
try {
|
|
StringBuilder sb = new StringBuilder(64);
|
|
int delimiterPos = 0;
|
|
int len = delimiter.length();
|
|
while (undecodedChunk.isReadable() && delimiterPos < len) {
|
|
byte nextByte = undecodedChunk.readByte();
|
|
if (nextByte == delimiter.charAt(delimiterPos)) {
|
|
delimiterPos++;
|
|
sb.append((char) nextByte);
|
|
} else {
|
|
// delimiter not found so break here !
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
}
|
|
// Now check if either opening delimiter or closing delimiter
|
|
if (undecodedChunk.isReadable()) {
|
|
byte nextByte = undecodedChunk.readByte();
|
|
// first check for opening delimiter
|
|
if (nextByte == HttpConstants.CR) {
|
|
nextByte = undecodedChunk.readByte();
|
|
if (nextByte == HttpConstants.LF) {
|
|
return sb.toString();
|
|
} else {
|
|
// error since CR must be followed by LF
|
|
// delimiter not found so break here !
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
} else if (nextByte == HttpConstants.LF) {
|
|
return sb.toString();
|
|
} else if (nextByte == '-') {
|
|
sb.append('-');
|
|
// second check for closing delimiter
|
|
nextByte = undecodedChunk.readByte();
|
|
if (nextByte == '-') {
|
|
sb.append('-');
|
|
// now try to find if CRLF or LF there
|
|
if (undecodedChunk.isReadable()) {
|
|
nextByte = undecodedChunk.readByte();
|
|
if (nextByte == HttpConstants.CR) {
|
|
nextByte = undecodedChunk.readByte();
|
|
if (nextByte == HttpConstants.LF) {
|
|
return sb.toString();
|
|
} else {
|
|
// error CR without LF
|
|
// delimiter not found so break here !
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
} else if (nextByte == HttpConstants.LF) {
|
|
return sb.toString();
|
|
} else {
|
|
// No CRLF but ok however (Adobe Flash uploader)
|
|
// minus 1 since we read one char ahead but
|
|
// should not
|
|
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
|
|
return sb.toString();
|
|
}
|
|
}
|
|
// FIXME what do we do here?
|
|
// either considering it is fine, either waiting for
|
|
// more data to come?
|
|
// lets try considering it is fine...
|
|
return sb.toString();
|
|
}
|
|
// only one '-' => not enough
|
|
// whatever now => error since incomplete
|
|
}
|
|
}
|
|
} catch (IndexOutOfBoundsException e) {
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException(e);
|
|
}
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
|
|
/**
|
|
* Read one line up to --delimiter or --delimiter-- and if existing the CRLF
|
|
* or LF. Note that CRLF or LF are mandatory for opening delimiter
|
|
* (--delimiter) but not for closing delimiter (--delimiter--) since some
|
|
* clients does not include CRLF in this case.
|
|
*
|
|
* @param delimiter
|
|
* of the form --string, such that '--' is already included
|
|
* @return the String from one line as the delimiter searched (opening or
|
|
* closing)
|
|
* @throws NotEnoughDataDecoderException
|
|
* Need more chunks and reset the readerInder to the previous
|
|
* value
|
|
*/
|
|
private static String readDelimiter(ByteBuf undecodedChunk, String delimiter) {
|
|
if (!undecodedChunk.hasArray()) {
|
|
return readDelimiterStandard(undecodedChunk, delimiter);
|
|
}
|
|
SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
|
|
int readerIndex = undecodedChunk.readerIndex();
|
|
int delimiterPos = 0;
|
|
int len = delimiter.length();
|
|
try {
|
|
StringBuilder sb = new StringBuilder(64);
|
|
// check conformity with delimiter
|
|
while (sao.pos < sao.limit && delimiterPos < len) {
|
|
byte nextByte = sao.bytes[sao.pos++];
|
|
if (nextByte == delimiter.charAt(delimiterPos)) {
|
|
delimiterPos++;
|
|
sb.append((char) nextByte);
|
|
} else {
|
|
// delimiter not found so break here !
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
}
|
|
// Now check if either opening delimiter or closing delimiter
|
|
if (sao.pos < sao.limit) {
|
|
byte nextByte = sao.bytes[sao.pos++];
|
|
if (nextByte == HttpConstants.CR) {
|
|
// first check for opening delimiter
|
|
if (sao.pos < sao.limit) {
|
|
nextByte = sao.bytes[sao.pos++];
|
|
if (nextByte == HttpConstants.LF) {
|
|
sao.setReadPosition(0);
|
|
return sb.toString();
|
|
} else {
|
|
// error CR without LF
|
|
// delimiter not found so break here !
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
} else {
|
|
// error since CR must be followed by LF
|
|
// delimiter not found so break here !
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
} else if (nextByte == HttpConstants.LF) {
|
|
// same first check for opening delimiter where LF used with
|
|
// no CR
|
|
sao.setReadPosition(0);
|
|
return sb.toString();
|
|
} else if (nextByte == '-') {
|
|
sb.append('-');
|
|
// second check for closing delimiter
|
|
if (sao.pos < sao.limit) {
|
|
nextByte = sao.bytes[sao.pos++];
|
|
if (nextByte == '-') {
|
|
sb.append('-');
|
|
// now try to find if CRLF or LF there
|
|
if (sao.pos < sao.limit) {
|
|
nextByte = sao.bytes[sao.pos++];
|
|
if (nextByte == HttpConstants.CR) {
|
|
if (sao.pos < sao.limit) {
|
|
nextByte = sao.bytes[sao.pos++];
|
|
if (nextByte == HttpConstants.LF) {
|
|
sao.setReadPosition(0);
|
|
return sb.toString();
|
|
} else {
|
|
// error CR without LF
|
|
// delimiter not found so break here !
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
} else {
|
|
// error CR without LF
|
|
// delimiter not found so break here !
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
} else if (nextByte == HttpConstants.LF) {
|
|
sao.setReadPosition(0);
|
|
return sb.toString();
|
|
} else {
|
|
// No CRLF but ok however (Adobe Flash
|
|
// uploader)
|
|
// minus 1 since we read one char ahead but
|
|
// should not
|
|
sao.setReadPosition(1);
|
|
return sb.toString();
|
|
}
|
|
}
|
|
// FIXME what do we do here?
|
|
// either considering it is fine, either waiting for
|
|
// more data to come?
|
|
// lets try considering it is fine...
|
|
sao.setReadPosition(0);
|
|
return sb.toString();
|
|
}
|
|
// whatever now => error since incomplete
|
|
// only one '-' => not enough or whatever not enough
|
|
// element
|
|
}
|
|
}
|
|
}
|
|
} catch (IndexOutOfBoundsException e) {
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException(e);
|
|
}
|
|
undecodedChunk.readerIndex(readerIndex);
|
|
throw new NotEnoughDataDecoderException();
|
|
}
|
|
|
|
/**
|
|
* Load the field value or file data from a Multipart request
|
|
*
|
|
* @return {@code true} if the last chunk is loaded (boundary delimiter found), {@code false} if need more chunks
|
|
* @throws ErrorDataDecoderException
|
|
*/
|
|
private static boolean loadDataMultipartStandard(ByteBuf undecodedChunk, String delimiter, HttpData httpData) {
|
|
final int startReaderIndex = undecodedChunk.readerIndex();
|
|
final int delimeterLength = delimiter.length();
|
|
int index = 0;
|
|
int lastPosition = startReaderIndex;
|
|
byte prevByte = HttpConstants.LF;
|
|
boolean delimiterFound = false;
|
|
while (undecodedChunk.isReadable()) {
|
|
final byte nextByte = undecodedChunk.readByte();
|
|
// Check the delimiter
|
|
if (prevByte == HttpConstants.LF && nextByte == delimiter.codePointAt(index)) {
|
|
index++;
|
|
if (delimeterLength == index) {
|
|
delimiterFound = true;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
lastPosition = undecodedChunk.readerIndex();
|
|
if (nextByte == HttpConstants.LF) {
|
|
index = 0;
|
|
lastPosition -= (prevByte == HttpConstants.CR)? 2 : 1;
|
|
}
|
|
prevByte = nextByte;
|
|
}
|
|
if (prevByte == HttpConstants.CR) {
|
|
lastPosition--;
|
|
}
|
|
ByteBuf content = undecodedChunk.copy(startReaderIndex, lastPosition - startReaderIndex);
|
|
try {
|
|
httpData.addContent(content, delimiterFound);
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
undecodedChunk.readerIndex(lastPosition);
|
|
return delimiterFound;
|
|
}
|
|
|
|
/**
|
|
* Load the field value from a Multipart request
|
|
*
|
|
* @return {@code true} if the last chunk is loaded (boundary delimiter found), {@code false} if need more chunks
|
|
* @throws ErrorDataDecoderException
|
|
*/
|
|
private static boolean loadDataMultipart(ByteBuf undecodedChunk, String delimiter, HttpData httpData) {
|
|
if (!undecodedChunk.hasArray()) {
|
|
return loadDataMultipartStandard(undecodedChunk, delimiter, httpData);
|
|
}
|
|
final SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
|
|
final int startReaderIndex = undecodedChunk.readerIndex();
|
|
final int delimeterLength = delimiter.length();
|
|
int index = 0;
|
|
int lastRealPos = sao.pos;
|
|
byte prevByte = HttpConstants.LF;
|
|
boolean delimiterFound = false;
|
|
while (sao.pos < sao.limit) {
|
|
final byte nextByte = sao.bytes[sao.pos++];
|
|
// Check the delimiter
|
|
if (prevByte == HttpConstants.LF && nextByte == delimiter.codePointAt(index)) {
|
|
index++;
|
|
if (delimeterLength == index) {
|
|
delimiterFound = true;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
lastRealPos = sao.pos;
|
|
if (nextByte == HttpConstants.LF) {
|
|
index = 0;
|
|
lastRealPos -= (prevByte == HttpConstants.CR)? 2 : 1;
|
|
}
|
|
prevByte = nextByte;
|
|
}
|
|
if (prevByte == HttpConstants.CR) {
|
|
lastRealPos--;
|
|
}
|
|
final int lastPosition = sao.getReadPosition(lastRealPos);
|
|
final ByteBuf content = undecodedChunk.copy(startReaderIndex, lastPosition - startReaderIndex);
|
|
try {
|
|
httpData.addContent(content, delimiterFound);
|
|
} catch (IOException e) {
|
|
throw new ErrorDataDecoderException(e);
|
|
}
|
|
undecodedChunk.readerIndex(lastPosition);
|
|
return delimiterFound;
|
|
}
|
|
|
|
/**
|
|
* Clean the String from any unallowed character
|
|
*
|
|
* @return the cleaned String
|
|
*/
|
|
private static String cleanString(String field) {
|
|
int size = field.length();
|
|
StringBuilder sb = new StringBuilder(size);
|
|
for (int i = 0; i < size; i++) {
|
|
char nextChar = field.charAt(i);
|
|
switch (nextChar) {
|
|
case HttpConstants.COLON:
|
|
case HttpConstants.COMMA:
|
|
case HttpConstants.EQUALS:
|
|
case HttpConstants.SEMICOLON:
|
|
case HttpConstants.HT:
|
|
sb.append(HttpConstants.SP_CHAR);
|
|
break;
|
|
case HttpConstants.DOUBLE_QUOTE:
|
|
// nothing added, just removes it
|
|
break;
|
|
default:
|
|
sb.append(nextChar);
|
|
break;
|
|
}
|
|
}
|
|
return sb.toString().trim();
|
|
}
|
|
|
|
/**
|
|
* Skip one empty line
|
|
*
|
|
* @return True if one empty line was skipped
|
|
*/
|
|
private boolean skipOneLine() {
|
|
if (!undecodedChunk.isReadable()) {
|
|
return false;
|
|
}
|
|
byte nextByte = undecodedChunk.readByte();
|
|
if (nextByte == HttpConstants.CR) {
|
|
if (!undecodedChunk.isReadable()) {
|
|
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
|
|
return false;
|
|
}
|
|
nextByte = undecodedChunk.readByte();
|
|
if (nextByte == HttpConstants.LF) {
|
|
return true;
|
|
}
|
|
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 2);
|
|
return false;
|
|
}
|
|
if (nextByte == HttpConstants.LF) {
|
|
return true;
|
|
}
|
|
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Split one header in Multipart
|
|
*
|
|
* @return an array of String where rank 0 is the name of the header,
|
|
* follows by several values that were separated by ';' or ','
|
|
*/
|
|
private static String[] splitMultipartHeader(String sb) {
|
|
ArrayList<String> headers = new ArrayList<String>(1);
|
|
int nameStart;
|
|
int nameEnd;
|
|
int colonEnd;
|
|
int valueStart;
|
|
int valueEnd;
|
|
nameStart = HttpPostBodyUtil.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 = HttpPostBodyUtil.findNonWhitespace(sb, colonEnd);
|
|
valueEnd = HttpPostBodyUtil.findEndOfString(sb);
|
|
headers.add(sb.substring(nameStart, nameEnd));
|
|
String svalue = (valueStart >= valueEnd) ? StringUtil.EMPTY_STRING : sb.substring(valueStart, valueEnd);
|
|
String[] values;
|
|
if (svalue.indexOf(';') >= 0) {
|
|
values = splitMultipartHeaderValues(svalue);
|
|
} else {
|
|
values = svalue.split(",");
|
|
}
|
|
for (String value : values) {
|
|
headers.add(value.trim());
|
|
}
|
|
String[] array = new String[headers.size()];
|
|
for (int i = 0; i < headers.size(); i++) {
|
|
array[i] = headers.get(i);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/**
|
|
* Split one header value in Multipart
|
|
* @return an array of String where values that were separated by ';' or ','
|
|
*/
|
|
private static String[] splitMultipartHeaderValues(String svalue) {
|
|
List<String> values = InternalThreadLocalMap.get().arrayList(1);
|
|
boolean inQuote = false;
|
|
boolean escapeNext = false;
|
|
int start = 0;
|
|
for (int i = 0; i < svalue.length(); i++) {
|
|
char c = svalue.charAt(i);
|
|
if (inQuote) {
|
|
if (escapeNext) {
|
|
escapeNext = false;
|
|
} else {
|
|
if (c == '\\') {
|
|
escapeNext = true;
|
|
} else if (c == '"') {
|
|
inQuote = false;
|
|
}
|
|
}
|
|
} else {
|
|
if (c == '"') {
|
|
inQuote = true;
|
|
} else if (c == ';') {
|
|
values.add(svalue.substring(start, i));
|
|
start = i + 1;
|
|
}
|
|
}
|
|
}
|
|
values.add(svalue.substring(start));
|
|
return values.toArray(new String[0]);
|
|
}
|
|
}
|