From 651c7b056a386254165303a23fc9d4d917477ffb Mon Sep 17 00:00:00 2001 From: fredericBregier Date: Mon, 19 Aug 2013 23:22:22 +0200 Subject: [PATCH] Split HttpPostRequestDecoder into HttpPostStandardRequestDecoder and HttpPostMultipartRequestDecoder / Add HttpData.maxSize - Related issues: #1937 #1938 and #1946 - Add InterfaceHttpPostRequestDecoder and Make HttpPostRequestDecoder implement it - HttpPostRequestDecoder actually delegates itself to HttpPostStandardRequestDecoder or HttpPostMultipartRequestDecoder - Remove IncompatibleDataDecoderException because it's not thrown anywhere now --- .../http/multipart/AbstractDiskHttpData.java | 4 + .../http/multipart/AbstractHttpData.java | 11 + .../multipart/AbstractMemoryHttpData.java | 4 + .../multipart/DefaultHttpDataFactory.java | 47 +- .../codec/http/multipart/DiskAttribute.java | 2 + .../codec/http/multipart/HttpData.java | 17 + .../codec/http/multipart/HttpDataFactory.java | 7 + .../HttpPostMultipartRequestDecoder.java | 1747 +++++++++++++++ .../multipart/HttpPostRequestDecoder.java | 1993 +---------------- .../multipart/HttpPostRequestEncoder.java | 4 +- .../HttpPostStandardRequestDecoder.java | 728 ++++++ .../InterfaceHttpPostRequestDecoder.java | 140 ++ .../codec/http/multipart/MemoryAttribute.java | 2 + .../codec/http/multipart/MixedAttribute.java | 22 + .../codec/http/multipart/MixedFileUpload.java | 20 +- .../http/upload/HttpUploadServerHandler.java | 16 +- 16 files changed, 2805 insertions(+), 1959 deletions(-) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/InterfaceHttpPostRequestDecoder.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java index 62af2b449c..4ba8194d07 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java @@ -105,6 +105,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { } try { size = buffer.readableBytes(); + checkSize(size); if (definedSize > 0 && definedSize < size) { throw new IOException("Out of size: " + size + " > " + definedSize); } @@ -141,6 +142,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { if (buffer != null) { try { int localsize = buffer.readableBytes(); + checkSize(size + localsize); if (definedSize > 0 && definedSize < size + localsize) { throw new IOException("Out of size: " + (size + localsize) + " > " + definedSize); @@ -191,6 +193,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { } this.file = file; size = file.length(); + checkSize(size); isRenamed = true; completed = true; } @@ -213,6 +216,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData { while (read > 0) { byteBuffer.position(read).flip(); written += localfileChannel.write(byteBuffer); + checkSize(written); read = inputStream.read(bytes); } localfileChannel.force(false); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java index 5a3af0e6ad..c6a165b0fc 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java @@ -37,6 +37,7 @@ public abstract class AbstractHttpData extends AbstractReferenceCounted implemen protected long size; protected Charset charset = HttpConstants.DEFAULT_CHARSET; protected boolean completed; + protected long maxSize = DefaultHttpDataFactory.MAXSIZE; protected AbstractHttpData(String name, Charset charset, long size) { if (name == null) { @@ -57,6 +58,16 @@ public abstract class AbstractHttpData extends AbstractReferenceCounted implemen definedSize = size; } + public void setMaxSize(long maxSize) { + this.maxSize = maxSize; + } + + public void checkSize(long newSize) throws IOException { + if (maxSize >= 0 && newSize > maxSize) { + throw new IOException("Size exceed allowed maximum capacity"); + } + } + @Override public String getName() { return name; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java index 929226b8e2..fcc73bb3d9 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java @@ -49,6 +49,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { throw new NullPointerException("buffer"); } long localsize = buffer.readableBytes(); + checkSize(localsize); if (definedSize > 0 && definedSize < localsize) { throw new IOException("Out of size: " + localsize + " > " + definedSize); @@ -73,6 +74,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { while (read > 0) { buffer.writeBytes(bytes, 0, read); written += read; + checkSize(written); read = inputStream.read(bytes); } size = written; @@ -91,6 +93,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { throws IOException { if (buffer != null) { long localsize = buffer.readableBytes(); + checkSize(size + localsize); if (definedSize > 0 && definedSize < size + localsize) { throw new IOException("Out of size: " + (size + localsize) + " > " + definedSize); @@ -128,6 +131,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData { throw new IllegalArgumentException( "File too big to be loaded in memory"); } + checkSize(newsize); FileInputStream inputStream = new FileInputStream(file); FileChannel fileChannel = inputStream.getChannel(); byte[] array = new byte[(int) newsize]; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DefaultHttpDataFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DefaultHttpDataFactory.java index af4655385e..3e458cbfd5 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DefaultHttpDataFactory.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DefaultHttpDataFactory.java @@ -37,6 +37,10 @@ public class DefaultHttpDataFactory implements HttpDataFactory { * Proposed default MINSIZE as 16 KB. */ public static final long MINSIZE = 0x4000; + /** + * Proposed default MAXSIZE = -1 as UNLIMITED + */ + public static final long MAXSIZE = -1; private final boolean useDisk; @@ -44,6 +48,8 @@ public class DefaultHttpDataFactory implements HttpDataFactory { private long minSize; + private long maxSize = MAXSIZE; + /** * Keep all HttpDatas until cleanAllHttpDatas() is called. */ @@ -77,6 +83,10 @@ public class DefaultHttpDataFactory implements HttpDataFactory { this.minSize = minSize; } + public void setMaxLimit(long max) { + this.maxSize = max; + } + /** * @return the associated list of Files for the request */ @@ -93,17 +103,33 @@ public class DefaultHttpDataFactory implements HttpDataFactory { public Attribute createAttribute(HttpRequest request, String name) { if (useDisk) { Attribute attribute = new DiskAttribute(name); + attribute.setMaxSize(maxSize); List fileToDelete = getList(request); fileToDelete.add(attribute); return attribute; } if (checkSize) { Attribute attribute = new MixedAttribute(name, minSize); + attribute.setMaxSize(maxSize); List fileToDelete = getList(request); fileToDelete.add(attribute); return attribute; } - return new MemoryAttribute(name); + MemoryAttribute attribute = new MemoryAttribute(name); + attribute.setMaxSize(maxSize); + return attribute; + } + + /** + * Utility method + * @param data + */ + private void checkHttpDataSize(HttpData data) { + try { + data.checkSize(data.length()); + } catch (IOException e) { + throw new IllegalArgumentException("Attribute bigger than maxSize allowed"); + } } @Override @@ -112,22 +138,30 @@ public class DefaultHttpDataFactory implements HttpDataFactory { Attribute attribute; try { attribute = new DiskAttribute(name, value); + attribute.setMaxSize(maxSize); } catch (IOException e) { // revert to Mixed mode attribute = new MixedAttribute(name, value, minSize); + attribute.setMaxSize(maxSize); } + checkHttpDataSize(attribute); List fileToDelete = getList(request); fileToDelete.add(attribute); return attribute; } if (checkSize) { Attribute attribute = new MixedAttribute(name, value, minSize); + attribute.setMaxSize(maxSize); + checkHttpDataSize(attribute); List fileToDelete = getList(request); fileToDelete.add(attribute); return attribute; } try { - return new MemoryAttribute(name, value); + MemoryAttribute attribute = new MemoryAttribute(name, value); + attribute.setMaxSize(maxSize); + checkHttpDataSize(attribute); + return attribute; } catch (IOException e) { throw new IllegalArgumentException(e); } @@ -140,6 +174,8 @@ public class DefaultHttpDataFactory implements HttpDataFactory { if (useDisk) { FileUpload fileUpload = new DiskFileUpload(name, filename, contentType, contentTransferEncoding, charset, size); + fileUpload.setMaxSize(maxSize); + checkHttpDataSize(fileUpload); List fileToDelete = getList(request); fileToDelete.add(fileUpload); return fileUpload; @@ -147,12 +183,17 @@ public class DefaultHttpDataFactory implements HttpDataFactory { if (checkSize) { FileUpload fileUpload = new MixedFileUpload(name, filename, contentType, contentTransferEncoding, charset, size, minSize); + fileUpload.setMaxSize(maxSize); + checkHttpDataSize(fileUpload); List fileToDelete = getList(request); fileToDelete.add(fileUpload); return fileUpload; } - return new MemoryFileUpload(name, filename, contentType, + MemoryFileUpload fileUpload = new MemoryFileUpload(name, filename, contentType, contentTransferEncoding, charset, size); + fileUpload.setMaxSize(maxSize); + checkHttpDataSize(fileUpload); + return fileUpload; } @Override diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java index 28ac87044b..8acd5d9660 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java @@ -64,6 +64,7 @@ public class DiskAttribute extends AbstractDiskHttpData implements Attribute { throw new NullPointerException("value"); } byte [] bytes = value.getBytes(charset.name()); + checkSize(bytes.length); ByteBuf buffer = wrappedBuffer(bytes); if (definedSize > 0) { definedSize = buffer.readableBytes(); @@ -74,6 +75,7 @@ public class DiskAttribute extends AbstractDiskHttpData implements Attribute { @Override public void addContent(ByteBuf buffer, boolean last) throws IOException { int localsize = buffer.readableBytes(); + checkSize(size + localsize); if (definedSize > 0 && definedSize < size + localsize) { definedSize = size + localsize; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpData.java index 3a6c9b49d4..91884ab827 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpData.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpData.java @@ -27,6 +27,23 @@ import java.nio.charset.Charset; * Extended interface for InterfaceHttpData */ public interface HttpData extends InterfaceHttpData, ByteBufHolder { + /** + * Set the maxSize for this HttpData. When limit will be reached, an exception will be raised. + * Setting it to (-1) means no limitation. + * + * By default, to be set from the HttpDataFactory. + * @param maxSize + */ + void setMaxSize(long maxSize); + + /** + * Check if the new size is not reaching the max limit allowed. + * The limit is always computed in term of bytes. + * @param newSize + * @throws IOException + */ + void checkSize(long newSize) throws IOException; + /** * Set the content from the ChannelBuffer (erase any previous data) * diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpDataFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpDataFactory.java index eceee401d1..ab0305ecf2 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpDataFactory.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpDataFactory.java @@ -23,6 +23,13 @@ import java.nio.charset.Charset; * Interface to enable creation of InterfaceHttpData objects */ public interface HttpDataFactory { + /** + * To set a max size limitation on fields. Exceeding it will generate an ErrorDataDecoderException. + * A value of -1 means no limitation (default). + * @param max + */ + void setMaxLimit(long max); + /** * * @param request associated request diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java new file mode 100644 index 0000000000..1499eab738 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java @@ -0,0 +1,1747 @@ +/* + * 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.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadNoBackArrayException; +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.internal.StringUtil; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static io.netty.buffer.Unpooled.*; + +/** + * This decoder will decode Body and can handle POST BODY. + * + * You MUST 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 final Charset charset; + + /** + * Does the last chunk already received + */ + private boolean isLastChunk; + + /** + * HttpDatas from Body + */ + private final List bodyListHttpData = new ArrayList(); + + /** + * HttpDatas as Map from Body + */ + private final Map> bodyMapHttpData = new TreeMap>( + CaseIgnoringComparator.INSTANCE); + + /** + * The current channelBuffer + */ + private ByteBuf undecodedChunk; + + /** + * Body HttpDatas current position + */ + private int bodyListHttpDataRank; + + /** + * If multipart, this is the boundary for the flobal 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 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) throws ErrorDataDecoderException { + 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) + throws ErrorDataDecoderException { + 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) + throws ErrorDataDecoderException { + if (factory == null) { + throw new NullPointerException("factory"); + } + if (request == null) { + throw new NullPointerException("request"); + } + if (charset == null) { + throw new NullPointerException("charset"); + } + this.request = request; + this.charset = charset; + this.factory = factory; + // Fill default values + + setMultipart(this.request.headers().get(HttpHeaders.Names.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. + * @param contentType + * @throws ErrorDataDecoderException + * @throws ErrorDataDecoderException + */ + private void setMultipart(String contentType) throws ErrorDataDecoderException { + multipartDataBoundary = HttpPostRequestDecoder.getMultipartDataBoundary(contentType); + 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 + */ + 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. + */ + public void setDiscardThreshold(int discardThreshold) { + if (discardThreshold < 0) { + throw new IllegalArgumentException("discardThreshold must be >= 0"); + } + this.discardThreshold = discardThreshold; + } + + /** + * Return the threshold in bytes after which read data in the buffer should be discarded. + */ + public int getDiscardThreshold() { + return discardThreshold; + } + + /** + * This getMethod returns a List of all HttpDatas from body.
+ * + * 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 + */ + public List getBodyHttpDatas() throws NotEnoughDataDecoderException { + checkDestroyed(); + + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + return bodyListHttpData; + } + + /** + * This getMethod returns a List of all HttpDatas with the given name from + * body.
+ * + * 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 + */ + public List getBodyHttpDatas(String name) throws NotEnoughDataDecoderException { + checkDestroyed(); + + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + return bodyMapHttpData.get(name); + } + + /** + * This getMethod returns the first InterfaceHttpData with the given name from + * body.
+ * + * 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 + */ + public InterfaceHttpData getBodyHttpData(String name) throws NotEnoughDataDecoderException { + checkDestroyed(); + + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + List 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 + */ + public HttpPostMultipartRequestDecoder offer(HttpContent content) throws ErrorDataDecoderException { + 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 + */ + public boolean hasNext() throws EndOfDataDecoderException { + 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 + */ + public InterfaceHttpData next() throws EndOfDataDecoderException { + checkDestroyed(); + + if (hasNext()) { + return bodyListHttpData.get(bodyListHttpDataRank++); + } + return null; + } + + /** + * 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() throws ErrorDataDecoderException { + 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 datas = bodyMapHttpData.get(data.getName()); + if (datas == null) { + datas = new ArrayList(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() throws ErrorDataDecoderException { + 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
+ *
+ * NOTSTARTED PREAMBLE (
+ * (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*
+ * (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE
+ * (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+
+ * MIXEDCLOSEDELIMITER)*
+ * CLOSEDELIMITER)+ EPILOGUE
+ * + * 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) throws ErrorDataDecoderException { + 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(HttpHeaders.Values.CHARSET); + if (charsetAttribute != null) { + try { + localCharset = Charset.forName(charsetAttribute.getValue()); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + Attribute nameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.NAME); + if (currentAttribute == null) { + try { + 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 + try { + loadFieldMultipart(multipartDataBoundary); + } catch (NotEnoughDataDecoderException e) { + 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 + */ + void skipControlCharacters() throws NotEnoughDataDecoderException { + SeekAheadOptimize sao; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e) { + try { + skipControlCharactersStandard(); + } catch (IndexOutOfBoundsException e1) { + throw new NotEnoughDataDecoderException(e1); + } + return; + } + + 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"); + } + + void skipControlCharactersStandard() { + 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) throws ErrorDataDecoderException { + // --AaB03x or --AaB03x-- + int readerIndex = undecodedChunk.readerIndex(); + try { + skipControlCharacters(); + } catch (NotEnoughDataDecoderException e1) { + undecodedChunk.readerIndex(readerIndex); + return null; + } + skipOneLine(); + String newline; + try { + newline = readDelimiter(delimiter); + } catch (NotEnoughDataDecoderException e) { + 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() throws ErrorDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + if (currentStatus == MultiPartStatus.DISPOSITION) { + currentFieldAttributes = new TreeMap(CaseIgnoringComparator.INSTANCE); + } + // read many lines until empty line with newline found! Store all data + while (!skipOneLine()) { + String newline; + try { + skipControlCharacters(); + newline = readLine(); + } catch (NotEnoughDataDecoderException e) { + undecodedChunk.readerIndex(readerIndex); + return null; + } + String[] contents = splitMultipartHeader(newline); + if (contents[0].equalsIgnoreCase(HttpPostBodyUtil.CONTENT_DISPOSITION)) { + boolean checkSecondArg; + if (currentStatus == MultiPartStatus.DISPOSITION) { + checkSecondArg = contents[1].equalsIgnoreCase(HttpPostBodyUtil.FORM_DATA); + } else { + checkSecondArg = contents[1].equalsIgnoreCase(HttpPostBodyUtil.ATTACHMENT) + || contents[1].equalsIgnoreCase(HttpPostBodyUtil.FILE); + } + if (checkSecondArg) { + // read next values and store them in the map as Attribute + for (int i = 2; i < contents.length; i++) { + String[] values = StringUtil.split(contents[i], '='); + Attribute attribute; + try { + String name = cleanString(values[0]); + String value = values[1]; + + // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html + if (HttpPostBodyUtil.FILENAME.equals(name)) { + // filename value is quoted string so strip them + value = value.substring(1, value.length() - 1); + } else { + // otherwise we need to clean the value + value = cleanString(value); + } + attribute = factory.createAttribute(request, name, value); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(attribute.getName(), attribute); + } + } + } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING)) { + Attribute attribute; + try { + attribute = factory.createAttribute(request, HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, + cleanString(contents[1])); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, attribute); + } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_LENGTH)) { + Attribute attribute; + try { + attribute = factory.createAttribute(request, HttpHeaders.Names.CONTENT_LENGTH, + cleanString(contents[1])); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(HttpHeaders.Names.CONTENT_LENGTH, attribute); + } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_TYPE)) { + // Take care of possible "multipart/mixed" + if (contents[1].equalsIgnoreCase(HttpPostBodyUtil.MULTIPART_MIXED)) { + if (currentStatus == MultiPartStatus.DISPOSITION) { + String[] values = StringUtil.split(contents[2], '='); + multipartMixedBoundary = "--" + values[1]; + 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++) { + if (contents[i].toLowerCase().startsWith(HttpHeaders.Values.CHARSET)) { + String[] values = StringUtil.split(contents[i], '='); + Attribute attribute; + try { + attribute = factory.createAttribute(request, HttpHeaders.Values.CHARSET, + cleanString(values[1])); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(HttpHeaders.Values.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); + } + } + } + } else { + throw new ErrorDataDecoderException("Unknown Params: " + newline); + } + } + // Is it a FileUpload + Attribute filenameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.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"); + } + } + } + + /** + * 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) throws ErrorDataDecoderException { + // eventually restart from existing FileUpload + // Now get value according to Content-Type and Charset + Attribute encoding = currentFieldAttributes.get(HttpHeaders.Names.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 = HttpPostBodyUtil.US_ASCII; + } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT8.value())) { + localCharset = HttpPostBodyUtil.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(HttpHeaders.Values.CHARSET); + if (charsetAttribute != null) { + try { + localCharset = Charset.forName(charsetAttribute.getValue()); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + if (currentFileUpload == null) { + Attribute filenameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.FILENAME); + Attribute nameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.NAME); + Attribute contentTypeAttribute = currentFieldAttributes.get(HttpHeaders.Names.CONTENT_TYPE); + if (contentTypeAttribute == null) { + throw new ErrorDataDecoderException("Content-Type is absent but required"); + } + Attribute lengthAttribute = currentFieldAttributes.get(HttpHeaders.Names.CONTENT_LENGTH); + long size; + try { + size = lengthAttribute != null ? Long.parseLong(lengthAttribute.getValue()) : 0L; + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } catch (NumberFormatException e) { + size = 0; + } + try { + currentFileUpload = factory.createFileUpload(request, + cleanString(nameAttribute.getValue()), cleanString(filenameAttribute.getValue()), + contentTypeAttribute.getValue(), 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 + try { + readFileUploadByteMultipart(delimiter); + } catch (NotEnoughDataDecoderException e) { + // do not change the buffer position + // since some can be already saved into FileUpload + // So do not change the currentStatus + 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. + */ + 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. + */ + public void cleanFiles() { + checkDestroyed(); + + factory.cleanRequestHttpDatas(request); + } + + /** + * Remove the given FileUpload from the list of FileUploads to clean + */ + 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(HttpHeaders.Values.CHARSET); + currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_LENGTH); + currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING); + currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_TYPE); + currentFieldAttributes.remove(HttpPostBodyUtil.FILENAME); + } + + /** + * Read one line up to the CRLF or LF + * + * @return the String from one line + * @throws NotEnoughDataDecoderException + * Need more chunks and reset the readerInder to the previous + * value + */ + private String readLineStandard() throws NotEnoughDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + try { + ByteBuf line = buffer(64); + + while (undecodedChunk.isReadable()) { + byte nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.CR) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + return line.toString(charset); + } + } 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 readerInder to the previous + * value + */ + private String readLine() throws NotEnoughDataDecoderException { + SeekAheadOptimize sao; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + return readLineStandard(); + } + 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 { + 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 readerInder to the previous + * value + */ + private String readDelimiterStandard(String delimiter) throws NotEnoughDataDecoderException { + 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 String readDelimiter(String delimiter) throws NotEnoughDataDecoderException { + SeekAheadOptimize sao; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + return readDelimiterStandard(delimiter); + } + 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 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 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(); + } + + /** + * Read a FileUpload data as Byte (Binary) and add the bytes directly to the + * FileUpload. If the delimiter is found, the FileUpload is completed. + * + * @throws NotEnoughDataDecoderException + * Need more chunks but do not reset the readerInder since some + * values will be already added to the FileOutput + * @throws ErrorDataDecoderException + * write IO error occurs with the FileUpload + */ + private void readFileUploadByteMultipartStandard(String delimiter) throws NotEnoughDataDecoderException, + ErrorDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + // found the decoder limit + boolean newLine = true; + int index = 0; + int lastPosition = undecodedChunk.readerIndex(); + boolean found = false; + while (undecodedChunk.isReadable()) { + byte nextByte = undecodedChunk.readByte(); + if (newLine) { + // Check the delimiter + if (nextByte == delimiter.codePointAt(index)) { + index++; + if (delimiter.length() == index) { + found = true; + break; + } + continue; + } else { + newLine = false; + index = 0; + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (undecodedChunk.isReadable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } else { + // save last valid position + lastPosition = undecodedChunk.readerIndex() - 1; + + // Unread next byte. + undecodedChunk.readerIndex(lastPosition); + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + // save last valid position + lastPosition = undecodedChunk.readerIndex(); + } + } + } else { + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (undecodedChunk.isReadable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } else { + // save last valid position + lastPosition = undecodedChunk.readerIndex() - 1; + + // Unread next byte. + undecodedChunk.readerIndex(lastPosition); + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + // save last valid position + lastPosition = undecodedChunk.readerIndex(); + } + } + } + ByteBuf buffer = undecodedChunk.copy(readerIndex, lastPosition - readerIndex); + if (found) { + // found so lastPosition is correct and final + try { + currentFileUpload.addContent(buffer, true); + // just before the CRLF and delimiter + undecodedChunk.readerIndex(lastPosition); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } else { + // possibly the delimiter is partially found but still the last + // position is OK + try { + currentFileUpload.addContent(buffer, false); + // last valid char (not CR, not LF, not beginning of delimiter) + undecodedChunk.readerIndex(lastPosition); + throw new NotEnoughDataDecoderException(); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + } + + /** + * Read a FileUpload data as Byte (Binary) and add the bytes directly to the + * FileUpload. If the delimiter is found, the FileUpload is completed. + * + * @throws NotEnoughDataDecoderException + * Need more chunks but do not reset the readerInder since some + * values will be already added to the FileOutput + * @throws ErrorDataDecoderException + * write IO error occurs with the FileUpload + */ + private void readFileUploadByteMultipart(String delimiter) throws NotEnoughDataDecoderException, + ErrorDataDecoderException { + SeekAheadOptimize sao; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + readFileUploadByteMultipartStandard(delimiter); + return; + } + int readerIndex = undecodedChunk.readerIndex(); + // found the decoder limit + boolean newLine = true; + int index = 0; + int lastrealpos = sao.pos; + int lastPosition; + boolean found = false; + + while (sao.pos < sao.limit) { + byte nextByte = sao.bytes[sao.pos++]; + if (newLine) { + // Check the delimiter + if (nextByte == delimiter.codePointAt(index)) { + index++; + if (delimiter.length() == index) { + found = true; + break; + } + continue; + } else { + newLine = false; + index = 0; + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 2; + } else { + // unread next byte + sao.pos--; + + // save last valid position + lastrealpos = sao.pos; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 1; + } else { + // save last valid position + lastrealpos = sao.pos; + } + } + } else { + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 2; + } else { + // unread next byte + sao.pos--; + + // save last valid position + lastrealpos = sao.pos; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 1; + } else { + // save last valid position + lastrealpos = sao.pos; + } + } + } + lastPosition = sao.getReadPosition(lastrealpos); + ByteBuf buffer = undecodedChunk.copy(readerIndex, lastPosition - readerIndex); + if (found) { + // found so lastPosition is correct and final + try { + currentFileUpload.addContent(buffer, true); + // just before the CRLF and delimiter + undecodedChunk.readerIndex(lastPosition); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } else { + // possibly the delimiter is partially found but still the last + // position is OK + try { + currentFileUpload.addContent(buffer, false); + // last valid char (not CR, not LF, not beginning of delimiter) + undecodedChunk.readerIndex(lastPosition); + throw new NotEnoughDataDecoderException(); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + } + + /** + * Load the field value from a Multipart request + * + * @throws NotEnoughDataDecoderException + * Need more chunks + * @throws ErrorDataDecoderException + */ + private void loadFieldMultipartStandard(String delimiter) throws NotEnoughDataDecoderException, + ErrorDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + try { + // found the decoder limit + boolean newLine = true; + int index = 0; + int lastPosition = undecodedChunk.readerIndex(); + boolean found = false; + while (undecodedChunk.isReadable()) { + byte nextByte = undecodedChunk.readByte(); + if (newLine) { + // Check the delimiter + if (nextByte == delimiter.codePointAt(index)) { + index++; + if (delimiter.length() == index) { + found = true; + break; + } + continue; + } else { + newLine = false; + index = 0; + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (undecodedChunk.isReadable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + lastPosition = undecodedChunk.readerIndex(); + } + } + } else { + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (undecodedChunk.isReadable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + lastPosition = undecodedChunk.readerIndex(); + } + } + } + if (found) { + // found so lastPosition is correct + // but position is just after the delimiter (either close + // delimiter or simple one) + // so go back of delimiter size + try { + currentAttribute.addContent( + undecodedChunk.copy(readerIndex, lastPosition - readerIndex), true); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + undecodedChunk.readerIndex(lastPosition); + } else { + try { + currentAttribute.addContent( + undecodedChunk.copy(readerIndex, lastPosition - readerIndex), false); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + undecodedChunk.readerIndex(lastPosition); + throw new NotEnoughDataDecoderException(); + } + } catch (IndexOutOfBoundsException e) { + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(e); + } + } + + /** + * Load the field value from a Multipart request + * + * @throws NotEnoughDataDecoderException + * Need more chunks + * @throws ErrorDataDecoderException + */ + private void loadFieldMultipart(String delimiter) throws NotEnoughDataDecoderException, ErrorDataDecoderException { + SeekAheadOptimize sao; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + loadFieldMultipartStandard(delimiter); + return; + } + int readerIndex = undecodedChunk.readerIndex(); + try { + // found the decoder limit + boolean newLine = true; + int index = 0; + int lastPosition; + int lastrealpos = sao.pos; + boolean found = false; + + while (sao.pos < sao.limit) { + byte nextByte = sao.bytes[sao.pos++]; + if (newLine) { + // Check the delimiter + if (nextByte == delimiter.codePointAt(index)) { + index++; + if (delimiter.length() == index) { + found = true; + break; + } + continue; + } else { + newLine = false; + index = 0; + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 1; + } else { + lastrealpos = sao.pos; + } + } + } else { + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 1; + } else { + lastrealpos = sao.pos; + } + } + } + lastPosition = sao.getReadPosition(lastrealpos); + if (found) { + // found so lastPosition is correct + // but position is just after the delimiter (either close + // delimiter or simple one) + // so go back of delimiter size + try { + currentAttribute.addContent( + undecodedChunk.copy(readerIndex, lastPosition - readerIndex), true); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + undecodedChunk.readerIndex(lastPosition); + } else { + try { + currentAttribute.addContent( + undecodedChunk.copy(readerIndex, lastPosition - readerIndex), false); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + undecodedChunk.readerIndex(lastPosition); + throw new NotEnoughDataDecoderException(); + } + } catch (IndexOutOfBoundsException e) { + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(e); + } + } + + /** + * Clean the String from any unallowed character + * + * @return the cleaned String + */ + private static String cleanString(String field) { + StringBuilder sb = new StringBuilder(field.length()); + for (int i = 0; i < field.length(); i++) { + char nextChar = field.charAt(i); + if (nextChar == HttpConstants.COLON) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.COMMA) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.EQUALS) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.SEMICOLON) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.HT) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.DOUBLE_QUOTE) { + // nothing added, just removes it + } else { + sb.append(nextChar); + } + } + 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 headers = new ArrayList(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 = sb.substring(valueStart, valueEnd); + String[] values; + if (svalue.indexOf(';') >= 0) { + values = StringUtil.split(svalue, ';'); + } else { + values = StringUtil.split(svalue, ','); + } + 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; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java index fe21fb7262..97d88bae67 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java @@ -15,29 +15,15 @@ */ package io.netty.handler.codec.http.multipart; -import io.netty.buffer.ByteBuf; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.http.HttpConstants; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.LastHttpContent; -import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadNoBackArrayException; -import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize; -import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.TransferEncodingMechanism; import io.netty.util.internal.StringUtil; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import static io.netty.buffer.Unpooled.*; /** * This decoder will decode Body and can handle POST BODY. @@ -45,94 +31,9 @@ import static io.netty.buffer.Unpooled.*; * You MUST call {@link #destroy()} after completion to release all resources. * */ -public class HttpPostRequestDecoder { - private static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024; - - /** - * Factory used to create InterfaceHttpData - */ - private final HttpDataFactory factory; - - /** - * Request to decode - */ - private final HttpRequest request; - - /** - * Default charset to use - */ - private final Charset charset; - - /** - * Does request have a body to decode - */ - private boolean bodyToDecode; - - /** - * Does the last chunk already received - */ - private boolean isLastChunk; - - /** - * HttpDatas from Body - */ - private final List bodyListHttpData = new ArrayList(); - - /** - * HttpDatas as Map from Body - */ - private final Map> bodyMapHttpData = new TreeMap>( - CaseIgnoringComparator.INSTANCE); - - /** - * The current channelBuffer - */ - private ByteBuf undecodedChunk; - - /** - * Does this request is a Multipart request - */ - private boolean isMultipart; - - /** - * Body HttpDatas current position - */ - private int bodyListHttpDataRank; - - /** - * If multipart, this is the boundary for the flobal 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 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 = DEFAULT_DISCARD_THRESHOLD; +public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder { + protected static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024; + protected InterfaceHttpPostRequestDecoder decoder; /** * @@ -140,14 +41,11 @@ public class HttpPostRequestDecoder { * the request to decode * @throws NullPointerException * for request - * @throws IncompatibleDataDecoderException - * if the request has no body to decode * @throws ErrorDataDecoderException * if the default charset was wrong when decoding or other * errors */ - public HttpPostRequestDecoder(HttpRequest request) throws ErrorDataDecoderException, - IncompatibleDataDecoderException { + public HttpPostRequestDecoder(HttpRequest request) throws ErrorDataDecoderException { this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET); } @@ -159,14 +57,11 @@ public class HttpPostRequestDecoder { * the request to decode * @throws NullPointerException * for request or factory - * @throws IncompatibleDataDecoderException - * if the request has no body to decode * @throws ErrorDataDecoderException * if the default charset was wrong when decoding or other * errors */ - public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) throws ErrorDataDecoderException, - IncompatibleDataDecoderException { + public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) throws ErrorDataDecoderException { this(factory, request, HttpConstants.DEFAULT_CHARSET); } @@ -180,14 +75,12 @@ public class HttpPostRequestDecoder { * the charset to use as default * @throws NullPointerException * for request or charset or factory - * @throws IncompatibleDataDecoderException - * if the request has no body to decode * @throws ErrorDataDecoderException * if the default charset was wrong when decoding or other * errors */ public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) - throws ErrorDataDecoderException, IncompatibleDataDecoderException { + throws ErrorDataDecoderException { if (factory == null) { throw new NullPointerException("factory"); } @@ -197,31 +90,11 @@ public class HttpPostRequestDecoder { if (charset == null) { throw new NullPointerException("charset"); } - this.request = request; - HttpMethod method = request.getMethod(); - if (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) || method.equals(HttpMethod.PATCH)) { - bodyToDecode = true; - } - this.charset = charset; - this.factory = factory; // Fill default values - - String contentType = this.request.headers().get(HttpHeaders.Names.CONTENT_TYPE); - if (contentType != null) { - checkMultipart(contentType); + if (isMultipart(request)) { + decoder = new HttpPostMultipartRequestDecoder(factory, request, charset); } else { - isMultipart = false; - } - if (!bodyToDecode) { - throw new IncompatibleDataDecoderException("No Body to decode"); - } - if (request instanceof HttpContent) { - // Offer automatically if the given request is als type of HttpContent - // See #1089 - offer((HttpContent) request); - } else { - undecodedChunk = buffer(); - parseBody(); + decoder = new HttpPostStandardRequestDecoder(factory, request, charset); } } @@ -255,1797 +128,93 @@ public class HttpPostRequestDecoder { * * Once CLOSEDELIMITER is found, last getStatus is EPILOGUE */ - private enum MultiPartStatus { + protected enum MultiPartStatus { NOTSTARTED, PREAMBLE, HEADERDELIMITER, DISPOSITION, FIELD, FILEUPLOAD, MIXEDPREAMBLE, MIXEDDELIMITER, MIXEDDISPOSITION, MIXEDFILEUPLOAD, MIXEDCLOSEDELIMITER, CLOSEDELIMITER, PREEPILOGUE, EPILOGUE } /** - * Check from the request ContentType if this request is a Multipart - * request. + * Check if the given request is a multipart request + * @param request + * @return True if the request is a Multipart request + * @throws ErrorDataDecoderException */ - private void checkMultipart(String contentType) throws ErrorDataDecoderException { + public static boolean isMultipart(HttpRequest request) throws ErrorDataDecoderException { + if (request.headers().contains(HttpHeaders.Names.CONTENT_TYPE)) { + return getMultipartDataBoundary(request.headers().get(HttpHeaders.Names.CONTENT_TYPE)) != null; + } else { + return false; + } + } + + /** + * Check from the request ContentType if this request is a Multipart request. + * @return the multipartDataBoundary if it exists, else null + */ + protected static String getMultipartDataBoundary(String contentType) + throws ErrorDataDecoderException { // Check if Post using "multipart/form-data; boundary=--89421926422648" String[] headerContentType = splitHeaderContentType(contentType); - if (headerContentType[0].toLowerCase().startsWith(HttpHeaders.Values.MULTIPART_FORM_DATA) - && headerContentType[1].toLowerCase().startsWith(HttpHeaders.Values.BOUNDARY)) { + if (headerContentType[0].toLowerCase().startsWith( + HttpHeaders.Values.MULTIPART_FORM_DATA) && + headerContentType[1].toLowerCase().startsWith( + HttpHeaders.Values.BOUNDARY)) { String[] boundary = StringUtil.split(headerContentType[1], '='); if (boundary.length != 2) { throw new ErrorDataDecoderException("Needs a boundary value"); } - multipartDataBoundary = "--" + boundary[1]; - isMultipart = true; - currentStatus = MultiPartStatus.HEADERDELIMITER; + return "--" + boundary[1]; } else { - isMultipart = false; + return null; } } - private void checkDestroyed() { - if (destroyed) { - throw new IllegalStateException(HttpPostRequestDecoder.class.getSimpleName() + " was destroyed already"); - } - } - - /** - * True if this request is a Multipart request - * - * @return True if this request is a Multipart request - */ public boolean isMultipart() { - checkDestroyed(); - return isMultipart; + return decoder.isMultipart(); } - /** - * 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. - */ public void setDiscardThreshold(int discardThreshold) { - if (discardThreshold < 0) { - throw new IllegalArgumentException("discardThreshold must be >= 0"); - } - this.discardThreshold = discardThreshold; + decoder.setDiscardThreshold(discardThreshold); } - /** - * Return the threshold in bytes after which read data in the buffer should be discarded. - */ public int getDiscardThreshold() { - return discardThreshold; + return decoder.getDiscardThreshold(); } - /** - * This getMethod returns a List of all HttpDatas from body.
- * - * 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 - */ public List getBodyHttpDatas() throws NotEnoughDataDecoderException { - checkDestroyed(); - - if (!isLastChunk) { - throw new NotEnoughDataDecoderException(); - } - return bodyListHttpData; + return decoder.getBodyHttpDatas(); } - /** - * This getMethod returns a List of all HttpDatas with the given name from - * body.
- * - * 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 - */ public List getBodyHttpDatas(String name) throws NotEnoughDataDecoderException { - checkDestroyed(); - - if (!isLastChunk) { - throw new NotEnoughDataDecoderException(); - } - return bodyMapHttpData.get(name); + return decoder.getBodyHttpDatas(name); } - /** - * This getMethod returns the first InterfaceHttpData with the given name from - * body.
- * - * 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 - */ public InterfaceHttpData getBodyHttpData(String name) throws NotEnoughDataDecoderException { - checkDestroyed(); - - if (!isLastChunk) { - throw new NotEnoughDataDecoderException(); - } - List list = bodyMapHttpData.get(name); - if (list != null) { - return list.get(0); - } - return null; + return decoder.getBodyHttpData(name); } - /** - * 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 - */ - public HttpPostRequestDecoder offer(HttpContent content) throws ErrorDataDecoderException { - 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; + public InterfaceHttpPostRequestDecoder offer(HttpContent content) throws ErrorDataDecoderException { + return decoder.offer(content); } - /** - * 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 - */ public boolean hasNext() throws EndOfDataDecoderException { - checkDestroyed(); - - if (currentStatus == MultiPartStatus.EPILOGUE) { - // OK except if end of list - if (bodyListHttpDataRank >= bodyListHttpData.size()) { - throw new EndOfDataDecoderException(); - } - } - return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size(); + return decoder.hasNext(); } - /** - * 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 - */ public InterfaceHttpData next() throws EndOfDataDecoderException { - checkDestroyed(); - - if (hasNext()) { - return bodyListHttpData.get(bodyListHttpDataRank++); - } - return null; + return decoder.next(); } - /** - * 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() throws ErrorDataDecoderException { - if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) { - if (isLastChunk) { - currentStatus = MultiPartStatus.EPILOGUE; - } - return; - } - if (isMultipart) { - parseBodyMultipart(); - } else { - parseBodyAttributes(); - } - } - - /** - * Utility function to add a new decoded data - */ - protected void addHttpData(InterfaceHttpData data) { - if (data == null) { - return; - } - List datas = bodyMapHttpData.get(data.getName()); - if (datas == null) { - datas = new ArrayList(1); - bodyMapHttpData.put(data.getName(), datas); - } - datas.add(data); - bodyListHttpData.add(data); - } - - /** - * This getMethod fill the map and list with as much Attribute as possible from - * Body in not Multipart mode. - * - * @throws ErrorDataDecoderException - * if there is a problem with the charset decoding or other - * errors - */ - private void parseBodyAttributesStandard() throws ErrorDataDecoderException { - int firstpos = undecodedChunk.readerIndex(); - int currentpos = firstpos; - int equalpos; - int ampersandpos; - if (currentStatus == MultiPartStatus.NOTSTARTED) { - currentStatus = MultiPartStatus.DISPOSITION; - } - boolean contRead = true; - try { - while (undecodedChunk.isReadable() && contRead) { - char read = (char) undecodedChunk.readUnsignedByte(); - currentpos++; - switch (currentStatus) { - case DISPOSITION:// search '=' - if (read == '=') { - currentStatus = MultiPartStatus.FIELD; - equalpos = currentpos - 1; - String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset), - charset); - currentAttribute = factory.createAttribute(request, key); - firstpos = currentpos; - } else if (read == '&') { // special empty FIELD - currentStatus = MultiPartStatus.DISPOSITION; - ampersandpos = currentpos - 1; - String key = decodeAttribute( - undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset); - currentAttribute = factory.createAttribute(request, key); - currentAttribute.setValue(""); // empty - addHttpData(currentAttribute); - currentAttribute = null; - firstpos = currentpos; - contRead = true; - } - break; - case FIELD:// search '&' or end of line - if (read == '&') { - currentStatus = MultiPartStatus.DISPOSITION; - ampersandpos = currentpos - 1; - setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); - firstpos = currentpos; - contRead = true; - } else if (read == HttpConstants.CR) { - if (undecodedChunk.isReadable()) { - read = (char) undecodedChunk.readUnsignedByte(); - currentpos++; - if (read == HttpConstants.LF) { - currentStatus = MultiPartStatus.PREEPILOGUE; - ampersandpos = currentpos - 2; - setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); - firstpos = currentpos; - contRead = false; - } else { - // Error - throw new ErrorDataDecoderException("Bad end of line"); - } - } else { - currentpos--; - } - } else if (read == HttpConstants.LF) { - currentStatus = MultiPartStatus.PREEPILOGUE; - ampersandpos = currentpos - 1; - setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); - firstpos = currentpos; - contRead = false; - } - break; - default: - // just stop - contRead = false; - } - } - if (isLastChunk && currentAttribute != null) { - // special case - ampersandpos = currentpos; - if (ampersandpos > firstpos) { - setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); - } else if (!currentAttribute.isCompleted()) { - setFinalBuffer(EMPTY_BUFFER); - } - firstpos = currentpos; - currentStatus = MultiPartStatus.EPILOGUE; - return; - } - if (contRead && currentAttribute != null) { - // reset index except if to continue in case of FIELD getStatus - if (currentStatus == MultiPartStatus.FIELD) { - currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos), - false); - firstpos = currentpos; - } - undecodedChunk.readerIndex(firstpos); - } else { - // end of line so keep index - } - } catch (ErrorDataDecoderException e) { - // error while decoding - undecodedChunk.readerIndex(firstpos); - throw e; - } catch (IOException e) { - // error while decoding - undecodedChunk.readerIndex(firstpos); - throw new ErrorDataDecoderException(e); - } - } - - /** - * This getMethod fill the map and list with as much Attribute as possible from - * Body in not Multipart mode. - * - * @throws ErrorDataDecoderException - * if there is a problem with the charset decoding or other - * errors - */ - private void parseBodyAttributes() throws ErrorDataDecoderException { - SeekAheadOptimize sao; - try { - sao = new SeekAheadOptimize(undecodedChunk); - } catch (SeekAheadNoBackArrayException e1) { - parseBodyAttributesStandard(); - return; - } - int firstpos = undecodedChunk.readerIndex(); - int currentpos = firstpos; - int equalpos; - int ampersandpos; - if (currentStatus == MultiPartStatus.NOTSTARTED) { - currentStatus = MultiPartStatus.DISPOSITION; - } - boolean contRead = true; - try { - loop: while (sao.pos < sao.limit) { - char read = (char) (sao.bytes[sao.pos++] & 0xFF); - currentpos++; - switch (currentStatus) { - case DISPOSITION:// search '=' - if (read == '=') { - currentStatus = MultiPartStatus.FIELD; - equalpos = currentpos - 1; - String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset), - charset); - currentAttribute = factory.createAttribute(request, key); - firstpos = currentpos; - } else if (read == '&') { // special empty FIELD - currentStatus = MultiPartStatus.DISPOSITION; - ampersandpos = currentpos - 1; - String key = decodeAttribute( - undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset); - currentAttribute = factory.createAttribute(request, key); - currentAttribute.setValue(""); // empty - addHttpData(currentAttribute); - currentAttribute = null; - firstpos = currentpos; - contRead = true; - } - break; - case FIELD:// search '&' or end of line - if (read == '&') { - currentStatus = MultiPartStatus.DISPOSITION; - ampersandpos = currentpos - 1; - setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); - firstpos = currentpos; - contRead = true; - } else if (read == HttpConstants.CR) { - if (sao.pos < sao.limit) { - read = (char) (sao.bytes[sao.pos++] & 0xFF); - currentpos++; - if (read == HttpConstants.LF) { - currentStatus = MultiPartStatus.PREEPILOGUE; - ampersandpos = currentpos - 2; - sao.setReadPosition(0); - setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); - firstpos = currentpos; - contRead = false; - break loop; - } else { - // Error - sao.setReadPosition(0); - throw new ErrorDataDecoderException("Bad end of line"); - } - } else { - if (sao.limit > 0) { - currentpos--; - } - } - } else if (read == HttpConstants.LF) { - currentStatus = MultiPartStatus.PREEPILOGUE; - ampersandpos = currentpos - 1; - sao.setReadPosition(0); - setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); - firstpos = currentpos; - contRead = false; - break loop; - } - break; - default: - // just stop - sao.setReadPosition(0); - contRead = false; - break loop; - } - } - if (isLastChunk && currentAttribute != null) { - // special case - ampersandpos = currentpos; - if (ampersandpos > firstpos) { - setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); - } else if (!currentAttribute.isCompleted()) { - setFinalBuffer(EMPTY_BUFFER); - } - firstpos = currentpos; - currentStatus = MultiPartStatus.EPILOGUE; - return; - } - if (contRead && currentAttribute != null) { - // reset index except if to continue in case of FIELD getStatus - if (currentStatus == MultiPartStatus.FIELD) { - currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos), - false); - firstpos = currentpos; - } - undecodedChunk.readerIndex(firstpos); - } else { - // end of line so keep index - } - } catch (ErrorDataDecoderException e) { - // error while decoding - undecodedChunk.readerIndex(firstpos); - throw e; - } catch (IOException e) { - // error while decoding - undecodedChunk.readerIndex(firstpos); - throw new ErrorDataDecoderException(e); - } - } - - private void setFinalBuffer(ByteBuf buffer) throws ErrorDataDecoderException, IOException { - currentAttribute.addContent(buffer, true); - String value = decodeAttribute(currentAttribute.getByteBuf().toString(charset), charset); - currentAttribute.setValue(value); - addHttpData(currentAttribute); - currentAttribute = null; - } - - /** - * Decode component - * - * @return the decoded component - */ - private static String decodeAttribute(String s, Charset charset) throws ErrorDataDecoderException { - if (s == null) { - return ""; - } - try { - return URLDecoder.decode(s, charset.name()); - } catch (UnsupportedEncodingException e) { - throw new ErrorDataDecoderException(charset.toString(), e); - } catch (IllegalArgumentException e) { - throw new ErrorDataDecoderException("Bad string: '" + s + '\'', e); - } - } - - /** - * Parse the Body for multipart - * - * @throws ErrorDataDecoderException - * if there is a problem with the charset decoding or other - * errors - */ - private void parseBodyMultipart() throws ErrorDataDecoderException { - 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
- *
- * NOTSTARTED PREAMBLE (
- * (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*
- * (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE
- * (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+
- * MIXEDCLOSEDELIMITER)*
- * CLOSEDELIMITER)+ EPILOGUE
- * - * 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) throws ErrorDataDecoderException { - 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(HttpHeaders.Values.CHARSET); - if (charsetAttribute != null) { - try { - localCharset = Charset.forName(charsetAttribute.getValue()); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - } - Attribute nameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.NAME); - if (currentAttribute == null) { - try { - 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 - try { - loadFieldMultipart(multipartDataBoundary); - } catch (NotEnoughDataDecoderException e) { - 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 - */ - void skipControlCharacters() throws NotEnoughDataDecoderException { - SeekAheadOptimize sao; - try { - sao = new SeekAheadOptimize(undecodedChunk); - } catch (SeekAheadNoBackArrayException e) { - try { - skipControlCharactersStandard(); - } catch (IndexOutOfBoundsException e1) { - throw new NotEnoughDataDecoderException(e1); - } - return; - } - - 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"); - } - - void skipControlCharactersStandard() { - 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) throws ErrorDataDecoderException { - // --AaB03x or --AaB03x-- - int readerIndex = undecodedChunk.readerIndex(); - try { - skipControlCharacters(); - } catch (NotEnoughDataDecoderException e1) { - undecodedChunk.readerIndex(readerIndex); - return null; - } - skipOneLine(); - String newline; - try { - newline = readDelimiter(delimiter); - } catch (NotEnoughDataDecoderException e) { - 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() throws ErrorDataDecoderException { - int readerIndex = undecodedChunk.readerIndex(); - if (currentStatus == MultiPartStatus.DISPOSITION) { - currentFieldAttributes = new TreeMap(CaseIgnoringComparator.INSTANCE); - } - // read many lines until empty line with newline found! Store all data - while (!skipOneLine()) { - String newline; - try { - skipControlCharacters(); - newline = readLine(); - } catch (NotEnoughDataDecoderException e) { - undecodedChunk.readerIndex(readerIndex); - return null; - } - String[] contents = splitMultipartHeader(newline); - if (contents[0].equalsIgnoreCase(HttpPostBodyUtil.CONTENT_DISPOSITION)) { - boolean checkSecondArg; - if (currentStatus == MultiPartStatus.DISPOSITION) { - checkSecondArg = contents[1].equalsIgnoreCase(HttpPostBodyUtil.FORM_DATA); - } else { - checkSecondArg = contents[1].equalsIgnoreCase(HttpPostBodyUtil.ATTACHMENT) - || contents[1].equalsIgnoreCase(HttpPostBodyUtil.FILE); - } - if (checkSecondArg) { - // read next values and store them in the map as Attribute - for (int i = 2; i < contents.length; i++) { - String[] values = StringUtil.split(contents[i], '='); - Attribute attribute; - try { - String name = cleanString(values[0]); - String value = values[1]; - - // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html - if (HttpPostBodyUtil.FILENAME.equals(name)) { - // filename value is quoted string so strip them - value = value.substring(1, value.length() - 1); - } else { - // otherwise we need to clean the value - value = cleanString(value); - } - attribute = factory.createAttribute(request, name, value); - } catch (NullPointerException e) { - throw new ErrorDataDecoderException(e); - } catch (IllegalArgumentException e) { - throw new ErrorDataDecoderException(e); - } - currentFieldAttributes.put(attribute.getName(), attribute); - } - } - } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING)) { - Attribute attribute; - try { - attribute = factory.createAttribute(request, HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, - cleanString(contents[1])); - } catch (NullPointerException e) { - throw new ErrorDataDecoderException(e); - } catch (IllegalArgumentException e) { - throw new ErrorDataDecoderException(e); - } - currentFieldAttributes.put(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, attribute); - } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_LENGTH)) { - Attribute attribute; - try { - attribute = factory.createAttribute(request, HttpHeaders.Names.CONTENT_LENGTH, - cleanString(contents[1])); - } catch (NullPointerException e) { - throw new ErrorDataDecoderException(e); - } catch (IllegalArgumentException e) { - throw new ErrorDataDecoderException(e); - } - currentFieldAttributes.put(HttpHeaders.Names.CONTENT_LENGTH, attribute); - } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_TYPE)) { - // Take care of possible "multipart/mixed" - if (contents[1].equalsIgnoreCase(HttpPostBodyUtil.MULTIPART_MIXED)) { - if (currentStatus == MultiPartStatus.DISPOSITION) { - String[] values = StringUtil.split(contents[2], '='); - multipartMixedBoundary = "--" + values[1]; - 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++) { - if (contents[i].toLowerCase().startsWith(HttpHeaders.Values.CHARSET)) { - String[] values = StringUtil.split(contents[i], '='); - Attribute attribute; - try { - attribute = factory.createAttribute(request, HttpHeaders.Values.CHARSET, - cleanString(values[1])); - } catch (NullPointerException e) { - throw new ErrorDataDecoderException(e); - } catch (IllegalArgumentException e) { - throw new ErrorDataDecoderException(e); - } - currentFieldAttributes.put(HttpHeaders.Values.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); - } - } - } - } else { - throw new ErrorDataDecoderException("Unknown Params: " + newline); - } - } - // Is it a FileUpload - Attribute filenameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.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"); - } - } - } - - /** - * 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) throws ErrorDataDecoderException { - // eventually restart from existing FileUpload - // Now get value according to Content-Type and Charset - Attribute encoding = currentFieldAttributes.get(HttpHeaders.Names.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 = HttpPostBodyUtil.US_ASCII; - } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT8.value())) { - localCharset = HttpPostBodyUtil.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(HttpHeaders.Values.CHARSET); - if (charsetAttribute != null) { - try { - localCharset = Charset.forName(charsetAttribute.getValue()); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - } - if (currentFileUpload == null) { - Attribute filenameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.FILENAME); - Attribute nameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.NAME); - Attribute contentTypeAttribute = currentFieldAttributes.get(HttpHeaders.Names.CONTENT_TYPE); - if (contentTypeAttribute == null) { - throw new ErrorDataDecoderException("Content-Type is absent but required"); - } - Attribute lengthAttribute = currentFieldAttributes.get(HttpHeaders.Names.CONTENT_LENGTH); - long size; - try { - size = lengthAttribute != null ? Long.parseLong(lengthAttribute.getValue()) : 0L; - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } catch (NumberFormatException e) { - size = 0; - } - try { - currentFileUpload = factory.createFileUpload(request, - cleanString(nameAttribute.getValue()), cleanString(filenameAttribute.getValue()), - contentTypeAttribute.getValue(), 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 - try { - readFileUploadByteMultipart(delimiter); - } catch (NotEnoughDataDecoderException e) { - // do not change the buffer position - // since some can be already saved into FileUpload - // So do not change the currentStatus - 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 HttpPostRequestDecoder} and release all it resources. After this method - * was called it is not possible to operate on it anymore. - */ 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(); - } + decoder.destroy(); } - /** - * Clean all HttpDatas (on Disk) for the current request. - */ public void cleanFiles() { - checkDestroyed(); - - factory.cleanRequestHttpDatas(request); + decoder.cleanFiles(); } - /** - * Remove the given FileUpload from the list of FileUploads to clean - */ 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(HttpHeaders.Values.CHARSET); - currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_LENGTH); - currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING); - currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_TYPE); - currentFieldAttributes.remove(HttpPostBodyUtil.FILENAME); - } - - /** - * Read one line up to the CRLF or LF - * - * @return the String from one line - * @throws NotEnoughDataDecoderException - * Need more chunks and reset the readerInder to the previous - * value - */ - private String readLineStandard() throws NotEnoughDataDecoderException { - int readerIndex = undecodedChunk.readerIndex(); - try { - ByteBuf line = buffer(64); - - while (undecodedChunk.isReadable()) { - byte nextByte = undecodedChunk.readByte(); - if (nextByte == HttpConstants.CR) { - nextByte = undecodedChunk.readByte(); - if (nextByte == HttpConstants.LF) { - return line.toString(charset); - } - } 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 readerInder to the previous - * value - */ - private String readLine() throws NotEnoughDataDecoderException { - SeekAheadOptimize sao; - try { - sao = new SeekAheadOptimize(undecodedChunk); - } catch (SeekAheadNoBackArrayException e1) { - return readLineStandard(); - } - 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 { - 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 readerInder to the previous - * value - */ - private String readDelimiterStandard(String delimiter) throws NotEnoughDataDecoderException { - 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 String readDelimiter(String delimiter) throws NotEnoughDataDecoderException { - SeekAheadOptimize sao; - try { - sao = new SeekAheadOptimize(undecodedChunk); - } catch (SeekAheadNoBackArrayException e1) { - return readDelimiterStandard(delimiter); - } - 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 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 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(); - } - - /** - * Read a FileUpload data as Byte (Binary) and add the bytes directly to the - * FileUpload. If the delimiter is found, the FileUpload is completed. - * - * @throws NotEnoughDataDecoderException - * Need more chunks but do not reset the readerInder since some - * values will be already added to the FileOutput - * @throws ErrorDataDecoderException - * write IO error occurs with the FileUpload - */ - private void readFileUploadByteMultipartStandard(String delimiter) throws NotEnoughDataDecoderException, - ErrorDataDecoderException { - int readerIndex = undecodedChunk.readerIndex(); - // found the decoder limit - boolean newLine = true; - int index = 0; - int lastPosition = undecodedChunk.readerIndex(); - boolean found = false; - while (undecodedChunk.isReadable()) { - byte nextByte = undecodedChunk.readByte(); - if (newLine) { - // Check the delimiter - if (nextByte == delimiter.codePointAt(index)) { - index++; - if (delimiter.length() == index) { - found = true; - break; - } - continue; - } else { - newLine = false; - index = 0; - // continue until end of line - if (nextByte == HttpConstants.CR) { - if (undecodedChunk.isReadable()) { - nextByte = undecodedChunk.readByte(); - if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastPosition = undecodedChunk.readerIndex() - 2; - } else { - // save last valid position - lastPosition = undecodedChunk.readerIndex() - 1; - - // Unread next byte. - undecodedChunk.readerIndex(lastPosition); - } - } - } else if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastPosition = undecodedChunk.readerIndex() - 1; - } else { - // save last valid position - lastPosition = undecodedChunk.readerIndex(); - } - } - } else { - // continue until end of line - if (nextByte == HttpConstants.CR) { - if (undecodedChunk.isReadable()) { - nextByte = undecodedChunk.readByte(); - if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastPosition = undecodedChunk.readerIndex() - 2; - } else { - // save last valid position - lastPosition = undecodedChunk.readerIndex() - 1; - - // Unread next byte. - undecodedChunk.readerIndex(lastPosition); - } - } - } else if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastPosition = undecodedChunk.readerIndex() - 1; - } else { - // save last valid position - lastPosition = undecodedChunk.readerIndex(); - } - } - } - ByteBuf buffer = undecodedChunk.copy(readerIndex, lastPosition - readerIndex); - if (found) { - // found so lastPosition is correct and final - try { - currentFileUpload.addContent(buffer, true); - // just before the CRLF and delimiter - undecodedChunk.readerIndex(lastPosition); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - } else { - // possibly the delimiter is partially found but still the last - // position is OK - try { - currentFileUpload.addContent(buffer, false); - // last valid char (not CR, not LF, not beginning of delimiter) - undecodedChunk.readerIndex(lastPosition); - throw new NotEnoughDataDecoderException(); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - } - } - - /** - * Read a FileUpload data as Byte (Binary) and add the bytes directly to the - * FileUpload. If the delimiter is found, the FileUpload is completed. - * - * @throws NotEnoughDataDecoderException - * Need more chunks but do not reset the readerInder since some - * values will be already added to the FileOutput - * @throws ErrorDataDecoderException - * write IO error occurs with the FileUpload - */ - private void readFileUploadByteMultipart(String delimiter) throws NotEnoughDataDecoderException, - ErrorDataDecoderException { - SeekAheadOptimize sao; - try { - sao = new SeekAheadOptimize(undecodedChunk); - } catch (SeekAheadNoBackArrayException e1) { - readFileUploadByteMultipartStandard(delimiter); - return; - } - int readerIndex = undecodedChunk.readerIndex(); - // found the decoder limit - boolean newLine = true; - int index = 0; - int lastrealpos = sao.pos; - int lastPosition; - boolean found = false; - - while (sao.pos < sao.limit) { - byte nextByte = sao.bytes[sao.pos++]; - if (newLine) { - // Check the delimiter - if (nextByte == delimiter.codePointAt(index)) { - index++; - if (delimiter.length() == index) { - found = true; - break; - } - continue; - } else { - newLine = false; - index = 0; - // continue until end of line - if (nextByte == HttpConstants.CR) { - if (sao.pos < sao.limit) { - nextByte = sao.bytes[sao.pos++]; - if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastrealpos = sao.pos - 2; - } else { - // unread next byte - sao.pos--; - - // save last valid position - lastrealpos = sao.pos; - } - } - } else if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastrealpos = sao.pos - 1; - } else { - // save last valid position - lastrealpos = sao.pos; - } - } - } else { - // continue until end of line - if (nextByte == HttpConstants.CR) { - if (sao.pos < sao.limit) { - nextByte = sao.bytes[sao.pos++]; - if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastrealpos = sao.pos - 2; - } else { - // unread next byte - sao.pos--; - - // save last valid position - lastrealpos = sao.pos; - } - } - } else if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastrealpos = sao.pos - 1; - } else { - // save last valid position - lastrealpos = sao.pos; - } - } - } - lastPosition = sao.getReadPosition(lastrealpos); - ByteBuf buffer = undecodedChunk.copy(readerIndex, lastPosition - readerIndex); - if (found) { - // found so lastPosition is correct and final - try { - currentFileUpload.addContent(buffer, true); - // just before the CRLF and delimiter - undecodedChunk.readerIndex(lastPosition); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - } else { - // possibly the delimiter is partially found but still the last - // position is OK - try { - currentFileUpload.addContent(buffer, false); - // last valid char (not CR, not LF, not beginning of delimiter) - undecodedChunk.readerIndex(lastPosition); - throw new NotEnoughDataDecoderException(); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - } - } - - /** - * Load the field value from a Multipart request - * - * @throws NotEnoughDataDecoderException - * Need more chunks - * @throws ErrorDataDecoderException - */ - private void loadFieldMultipartStandard(String delimiter) throws NotEnoughDataDecoderException, - ErrorDataDecoderException { - int readerIndex = undecodedChunk.readerIndex(); - try { - // found the decoder limit - boolean newLine = true; - int index = 0; - int lastPosition = undecodedChunk.readerIndex(); - boolean found = false; - while (undecodedChunk.isReadable()) { - byte nextByte = undecodedChunk.readByte(); - if (newLine) { - // Check the delimiter - if (nextByte == delimiter.codePointAt(index)) { - index++; - if (delimiter.length() == index) { - found = true; - break; - } - continue; - } else { - newLine = false; - index = 0; - // continue until end of line - if (nextByte == HttpConstants.CR) { - if (undecodedChunk.isReadable()) { - nextByte = undecodedChunk.readByte(); - if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastPosition = undecodedChunk.readerIndex() - 2; - } - } - } else if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastPosition = undecodedChunk.readerIndex() - 1; - } else { - lastPosition = undecodedChunk.readerIndex(); - } - } - } else { - // continue until end of line - if (nextByte == HttpConstants.CR) { - if (undecodedChunk.isReadable()) { - nextByte = undecodedChunk.readByte(); - if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastPosition = undecodedChunk.readerIndex() - 2; - } - } - } else if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastPosition = undecodedChunk.readerIndex() - 1; - } else { - lastPosition = undecodedChunk.readerIndex(); - } - } - } - if (found) { - // found so lastPosition is correct - // but position is just after the delimiter (either close - // delimiter or simple one) - // so go back of delimiter size - try { - currentAttribute.addContent( - undecodedChunk.copy(readerIndex, lastPosition - readerIndex), true); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - undecodedChunk.readerIndex(lastPosition); - } else { - try { - currentAttribute.addContent( - undecodedChunk.copy(readerIndex, lastPosition - readerIndex), false); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - undecodedChunk.readerIndex(lastPosition); - throw new NotEnoughDataDecoderException(); - } - } catch (IndexOutOfBoundsException e) { - undecodedChunk.readerIndex(readerIndex); - throw new NotEnoughDataDecoderException(e); - } - } - - /** - * Load the field value from a Multipart request - * - * @throws NotEnoughDataDecoderException - * Need more chunks - * @throws ErrorDataDecoderException - */ - private void loadFieldMultipart(String delimiter) throws NotEnoughDataDecoderException, ErrorDataDecoderException { - SeekAheadOptimize sao; - try { - sao = new SeekAheadOptimize(undecodedChunk); - } catch (SeekAheadNoBackArrayException e1) { - loadFieldMultipartStandard(delimiter); - return; - } - int readerIndex = undecodedChunk.readerIndex(); - try { - // found the decoder limit - boolean newLine = true; - int index = 0; - int lastPosition; - int lastrealpos = sao.pos; - boolean found = false; - - while (sao.pos < sao.limit) { - byte nextByte = sao.bytes[sao.pos++]; - if (newLine) { - // Check the delimiter - if (nextByte == delimiter.codePointAt(index)) { - index++; - if (delimiter.length() == index) { - found = true; - break; - } - continue; - } else { - newLine = false; - index = 0; - // continue until end of line - if (nextByte == HttpConstants.CR) { - if (sao.pos < sao.limit) { - nextByte = sao.bytes[sao.pos++]; - if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastrealpos = sao.pos - 2; - } - } - } else if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastrealpos = sao.pos - 1; - } else { - lastrealpos = sao.pos; - } - } - } else { - // continue until end of line - if (nextByte == HttpConstants.CR) { - if (sao.pos < sao.limit) { - nextByte = sao.bytes[sao.pos++]; - if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastrealpos = sao.pos - 2; - } - } - } else if (nextByte == HttpConstants.LF) { - newLine = true; - index = 0; - lastrealpos = sao.pos - 1; - } else { - lastrealpos = sao.pos; - } - } - } - lastPosition = sao.getReadPosition(lastrealpos); - if (found) { - // found so lastPosition is correct - // but position is just after the delimiter (either close - // delimiter or simple one) - // so go back of delimiter size - try { - currentAttribute.addContent( - undecodedChunk.copy(readerIndex, lastPosition - readerIndex), true); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - undecodedChunk.readerIndex(lastPosition); - } else { - try { - currentAttribute.addContent( - undecodedChunk.copy(readerIndex, lastPosition - readerIndex), false); - } catch (IOException e) { - throw new ErrorDataDecoderException(e); - } - undecodedChunk.readerIndex(lastPosition); - throw new NotEnoughDataDecoderException(); - } - } catch (IndexOutOfBoundsException e) { - undecodedChunk.readerIndex(readerIndex); - throw new NotEnoughDataDecoderException(e); - } - } - - /** - * Clean the String from any unallowed character - * - * @return the cleaned String - */ - private static String cleanString(String field) { - StringBuilder sb = new StringBuilder(field.length()); - for (int i = 0; i < field.length(); i++) { - char nextChar = field.charAt(i); - if (nextChar == HttpConstants.COLON) { - sb.append(HttpConstants.SP); - } else if (nextChar == HttpConstants.COMMA) { - sb.append(HttpConstants.SP); - } else if (nextChar == HttpConstants.EQUALS) { - sb.append(HttpConstants.SP); - } else if (nextChar == HttpConstants.SEMICOLON) { - sb.append(HttpConstants.SP); - } else if (nextChar == HttpConstants.HT) { - sb.append(HttpConstants.SP); - } else if (nextChar == HttpConstants.DOUBLE_QUOTE) { - // nothing added, just removes it - } else { - sb.append(nextChar); - } - } - 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; + decoder.removeHttpDataFromClean(data); } /** @@ -2071,52 +240,6 @@ public class HttpPostRequestDecoder { return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd) }; } - /** - * 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 headers = new ArrayList(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 = sb.substring(valueStart, valueEnd); - String[] values; - if (svalue.indexOf(';') >= 0) { - values = StringUtil.split(svalue, ';'); - } else { - values = StringUtil.split(svalue, ','); - } - 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; - } - /** * Exception when try reading data from request in chunked format, and not * enough data are available (need more chunks) @@ -2168,26 +291,4 @@ public class HttpPostRequestDecoder { super(msg, cause); } } - - /** - * Exception when an unappropriated getMethod was called on a request - */ - public static class IncompatibleDataDecoderException extends DecoderException { - private static final long serialVersionUID = -953268047926250267L; - - public IncompatibleDataDecoderException() { - } - - public IncompatibleDataDecoderException(String msg) { - super(msg); - } - - public IncompatibleDataDecoderException(Throwable cause) { - super(cause); - } - - public IncompatibleDataDecoderException(String msg, Throwable cause) { - super(msg, cause); - } - } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java index 4765485348..a06fa39b41 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java @@ -188,7 +188,9 @@ public class HttpPostRequestEncoder implements ChunkedInput { if (charset == null) { throw new NullPointerException("charset"); } - if (request.getMethod() != HttpMethod.POST) { + HttpMethod method = request.getMethod(); + if (!(method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) + || method.equals(HttpMethod.PATCH) || method.equals(HttpMethod.OPTIONS))) { throw new ErrorDataEncoderException("Cannot create a Encoder if not a POST"); } this.request = request; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java new file mode 100644 index 0000000000..190bf4f177 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java @@ -0,0 +1,728 @@ +/* + * 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.HttpRequest; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadNoBackArrayException; +import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize; +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 java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static io.netty.buffer.Unpooled.*; + +/** + * This decoder will decode Body and can handle POST BODY. + * + * You MUST call {@link #destroy()} after completion to release all resources. + * + */ +public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestDecoder { + /** + * Factory used to create InterfaceHttpData + */ + private final HttpDataFactory factory; + + /** + * Request to decode + */ + private final HttpRequest request; + + /** + * Default charset to use + */ + private final Charset charset; + + /** + * Does the last chunk already received + */ + private boolean isLastChunk; + + /** + * HttpDatas from Body + */ + private final List bodyListHttpData = new ArrayList(); + + /** + * HttpDatas as Map from Body + */ + private final Map> bodyMapHttpData = new TreeMap>( + CaseIgnoringComparator.INSTANCE); + + /** + * The current channelBuffer + */ + private ByteBuf undecodedChunk; + + /** + * Body HttpDatas current position + */ + private int bodyListHttpDataRank; + + /** + * Current getStatus + */ + private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED; + + /** + * 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 HttpPostStandardRequestDecoder(HttpRequest request) + throws ErrorDataDecoderException { + 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 HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request) + throws ErrorDataDecoderException { + 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 HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) + throws ErrorDataDecoderException { + if (factory == null) { + throw new NullPointerException("factory"); + } + if (request == null) { + throw new NullPointerException("request"); + } + if (charset == null) { + throw new NullPointerException("charset"); + } + this.request = request; + this.charset = charset; + this.factory = factory; + if (request instanceof HttpContent) { + // Offer automatically if the given request is als type of HttpContent + // See #1089 + offer((HttpContent) request); + } else { + undecodedChunk = buffer(); + parseBody(); + } + } + + private void checkDestroyed() { + if (destroyed) { + throw new IllegalStateException(HttpPostStandardRequestDecoder.class.getSimpleName() + + " was destroyed already"); + } + } + + /** + * True if this request is a Multipart request + * + * @return True if this request is a Multipart request + */ + public boolean isMultipart() { + checkDestroyed(); + return false; + } + + /** + * 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. + */ + public void setDiscardThreshold(int discardThreshold) { + if (discardThreshold < 0) { + throw new IllegalArgumentException("discardThreshold must be >= 0"); + } + this.discardThreshold = discardThreshold; + } + + /** + * Return the threshold in bytes after which read data in the buffer should be discarded. + */ + public int getDiscardThreshold() { + return discardThreshold; + } + + /** + * This getMethod returns a List of all HttpDatas from body.
+ * + * 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 + */ + public List getBodyHttpDatas() throws NotEnoughDataDecoderException { + checkDestroyed(); + + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + return bodyListHttpData; + } + + /** + * This getMethod returns a List of all HttpDatas with the given name from + * body.
+ * + * 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 + */ + public List getBodyHttpDatas(String name) + throws NotEnoughDataDecoderException { + checkDestroyed(); + + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + return bodyMapHttpData.get(name); + } + + /** + * This getMethod returns the first InterfaceHttpData with the given name from + * body.
+ * + * 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 + */ + public InterfaceHttpData getBodyHttpData(String name) throws NotEnoughDataDecoderException { + checkDestroyed(); + + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + List 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 + */ + public HttpPostStandardRequestDecoder offer(HttpContent content) + throws ErrorDataDecoderException { + 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 + */ + public boolean hasNext() throws EndOfDataDecoderException { + 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 + */ + public InterfaceHttpData next() throws EndOfDataDecoderException { + checkDestroyed(); + + if (hasNext()) { + return bodyListHttpData.get(bodyListHttpDataRank++); + } + return null; + } + + /** + * 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() throws ErrorDataDecoderException { + if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) { + if (isLastChunk) { + currentStatus = MultiPartStatus.EPILOGUE; + } + return; + } + parseBodyAttributes(); + } + + /** + * Utility function to add a new decoded data + */ + protected void addHttpData(InterfaceHttpData data) { + if (data == null) { + return; + } + List datas = bodyMapHttpData.get(data.getName()); + if (datas == null) { + datas = new ArrayList(1); + bodyMapHttpData.put(data.getName(), datas); + } + datas.add(data); + bodyListHttpData.add(data); + } + + /** + * This getMethod fill the map and list with as much Attribute as possible from + * Body in not Multipart mode. + * + * @throws ErrorDataDecoderException + * if there is a problem with the charset decoding or other + * errors + */ + private void parseBodyAttributesStandard() throws ErrorDataDecoderException { + int firstpos = undecodedChunk.readerIndex(); + int currentpos = firstpos; + int equalpos; + int ampersandpos; + if (currentStatus == MultiPartStatus.NOTSTARTED) { + currentStatus = MultiPartStatus.DISPOSITION; + } + boolean contRead = true; + try { + while (undecodedChunk.isReadable() && contRead) { + char read = (char) undecodedChunk.readUnsignedByte(); + currentpos++; + switch (currentStatus) { + case DISPOSITION:// search '=' + if (read == '=') { + currentStatus = MultiPartStatus.FIELD; + equalpos = currentpos - 1; + String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset), + charset); + currentAttribute = factory.createAttribute(request, key); + firstpos = currentpos; + } else if (read == '&') { // special empty FIELD + currentStatus = MultiPartStatus.DISPOSITION; + ampersandpos = currentpos - 1; + String key = decodeAttribute( + undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset); + currentAttribute = factory.createAttribute(request, key); + currentAttribute.setValue(""); // empty + addHttpData(currentAttribute); + currentAttribute = null; + firstpos = currentpos; + contRead = true; + } + break; + case FIELD:// search '&' or end of line + if (read == '&') { + currentStatus = MultiPartStatus.DISPOSITION; + ampersandpos = currentpos - 1; + setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = true; + } else if (read == HttpConstants.CR) { + if (undecodedChunk.isReadable()) { + read = (char) undecodedChunk.readUnsignedByte(); + currentpos++; + if (read == HttpConstants.LF) { + currentStatus = MultiPartStatus.PREEPILOGUE; + ampersandpos = currentpos - 2; + setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = false; + } else { + // Error + throw new ErrorDataDecoderException("Bad end of line"); + } + } else { + currentpos--; + } + } else if (read == HttpConstants.LF) { + currentStatus = MultiPartStatus.PREEPILOGUE; + ampersandpos = currentpos - 1; + setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = false; + } + break; + default: + // just stop + contRead = false; + } + } + if (isLastChunk && currentAttribute != null) { + // special case + ampersandpos = currentpos; + if (ampersandpos > firstpos) { + setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); + } else if (!currentAttribute.isCompleted()) { + setFinalBuffer(EMPTY_BUFFER); + } + firstpos = currentpos; + currentStatus = MultiPartStatus.EPILOGUE; + return; + } + if (contRead && currentAttribute != null) { + // reset index except if to continue in case of FIELD getStatus + if (currentStatus == MultiPartStatus.FIELD) { + currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos), + false); + firstpos = currentpos; + } + undecodedChunk.readerIndex(firstpos); + } else { + // end of line so keep index + } + } catch (ErrorDataDecoderException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw e; + } catch (IOException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw new ErrorDataDecoderException(e); + } + } + + /** + * This getMethod fill the map and list with as much Attribute as possible from + * Body in not Multipart mode. + * + * @throws ErrorDataDecoderException + * if there is a problem with the charset decoding or other + * errors + */ + private void parseBodyAttributes() throws ErrorDataDecoderException { + SeekAheadOptimize sao; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + parseBodyAttributesStandard(); + return; + } + int firstpos = undecodedChunk.readerIndex(); + int currentpos = firstpos; + int equalpos; + int ampersandpos; + if (currentStatus == MultiPartStatus.NOTSTARTED) { + currentStatus = MultiPartStatus.DISPOSITION; + } + boolean contRead = true; + try { + loop: while (sao.pos < sao.limit) { + char read = (char) (sao.bytes[sao.pos++] & 0xFF); + currentpos++; + switch (currentStatus) { + case DISPOSITION:// search '=' + if (read == '=') { + currentStatus = MultiPartStatus.FIELD; + equalpos = currentpos - 1; + String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset), + charset); + currentAttribute = factory.createAttribute(request, key); + firstpos = currentpos; + } else if (read == '&') { // special empty FIELD + currentStatus = MultiPartStatus.DISPOSITION; + ampersandpos = currentpos - 1; + String key = decodeAttribute( + undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset); + currentAttribute = factory.createAttribute(request, key); + currentAttribute.setValue(""); // empty + addHttpData(currentAttribute); + currentAttribute = null; + firstpos = currentpos; + contRead = true; + } + break; + case FIELD:// search '&' or end of line + if (read == '&') { + currentStatus = MultiPartStatus.DISPOSITION; + ampersandpos = currentpos - 1; + setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = true; + } else if (read == HttpConstants.CR) { + if (sao.pos < sao.limit) { + read = (char) (sao.bytes[sao.pos++] & 0xFF); + currentpos++; + if (read == HttpConstants.LF) { + currentStatus = MultiPartStatus.PREEPILOGUE; + ampersandpos = currentpos - 2; + sao.setReadPosition(0); + setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = false; + break loop; + } else { + // Error + sao.setReadPosition(0); + throw new ErrorDataDecoderException("Bad end of line"); + } + } else { + if (sao.limit > 0) { + currentpos--; + } + } + } else if (read == HttpConstants.LF) { + currentStatus = MultiPartStatus.PREEPILOGUE; + ampersandpos = currentpos - 1; + sao.setReadPosition(0); + setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = false; + break loop; + } + break; + default: + // just stop + sao.setReadPosition(0); + contRead = false; + break loop; + } + } + if (isLastChunk && currentAttribute != null) { + // special case + ampersandpos = currentpos; + if (ampersandpos > firstpos) { + setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos)); + } else if (!currentAttribute.isCompleted()) { + setFinalBuffer(EMPTY_BUFFER); + } + firstpos = currentpos; + currentStatus = MultiPartStatus.EPILOGUE; + return; + } + if (contRead && currentAttribute != null) { + // reset index except if to continue in case of FIELD getStatus + if (currentStatus == MultiPartStatus.FIELD) { + currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos), + false); + firstpos = currentpos; + } + undecodedChunk.readerIndex(firstpos); + } else { + // end of line so keep index + } + } catch (ErrorDataDecoderException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw e; + } catch (IOException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw new ErrorDataDecoderException(e); + } + } + + private void setFinalBuffer(ByteBuf buffer) throws ErrorDataDecoderException, IOException { + currentAttribute.addContent(buffer, true); + String value = decodeAttribute(currentAttribute.getByteBuf().toString(charset), charset); + currentAttribute.setValue(value); + addHttpData(currentAttribute); + currentAttribute = null; + } + + /** + * Decode component + * + * @return the decoded component + */ + private static String decodeAttribute(String s, Charset charset) + throws ErrorDataDecoderException { + if (s == null) { + return ""; + } + try { + return URLDecoder.decode(s, charset.name()); + } catch (UnsupportedEncodingException e) { + throw new ErrorDataDecoderException(charset.toString(), e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException("Bad string: '" + s + '\'', e); + } + } + + /** + * Skip control Characters + * + * @throws NotEnoughDataDecoderException + */ + void skipControlCharacters() throws NotEnoughDataDecoderException { + SeekAheadOptimize sao; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e) { + try { + skipControlCharactersStandard(); + } catch (IndexOutOfBoundsException e1) { + throw new NotEnoughDataDecoderException(e1); + } + return; + } + + 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"); + } + + void skipControlCharactersStandard() { + for (;;) { + char c = (char) undecodedChunk.readUnsignedByte(); + if (!Character.isISOControl(c) && !Character.isWhitespace(c)) { + undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1); + break; + } + } + } + + /** + * Destroy the {@link HttpPostStandardRequestDecoder} and release all it resources. After this method + * was called it is not possible to operate on it anymore. + */ + 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. + */ + public void cleanFiles() { + checkDestroyed(); + + factory.cleanRequestHttpDatas(request); + } + + /** + * Remove the given FileUpload from the list of FileUploads to clean + */ + public void removeHttpDataFromClean(InterfaceHttpData data) { + checkDestroyed(); + + factory.removeHttpDataFromClean(request, data); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InterfaceHttpPostRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InterfaceHttpPostRequestDecoder.java new file mode 100644 index 0000000000..39fc9c6d02 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InterfaceHttpPostRequestDecoder.java @@ -0,0 +1,140 @@ +/* + * 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.handler.codec.http.HttpContent; + +import java.util.List; + +/** + * This decoder will decode Body and can handle POST BODY (or for PUT, PATCH or OPTIONS). + * + * You MUST call {@link #destroy()} after completion to release all resources. + * + */ +public interface InterfaceHttpPostRequestDecoder { + /** + * True if this request is a Multipart request + * + * @return True if this request is a Multipart request + */ + boolean isMultipart(); + + /** + * 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. + */ + void setDiscardThreshold(int discardThreshold); + + /** + * Return the threshold in bytes after which read data in the buffer should be discarded. + */ + int getDiscardThreshold(); + + /** + * This getMethod returns a List of all HttpDatas from body.
+ * + * 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 HttpPostRequestDecoder.NotEnoughDataDecoderException + * Need more chunks + */ + List getBodyHttpDatas() throws HttpPostRequestDecoder.NotEnoughDataDecoderException; + + /** + * This getMethod returns a List of all HttpDatas with the given name from + * body.
+ * + * 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 HttpPostRequestDecoder.NotEnoughDataDecoderException + * need more chunks + */ + List getBodyHttpDatas(String name) throws HttpPostRequestDecoder.NotEnoughDataDecoderException; + + /** + * This getMethod returns the first InterfaceHttpData with the given name from + * body.
+ * + * 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 HttpPostRequestDecoder.NotEnoughDataDecoderException + * need more chunks + */ + InterfaceHttpData getBodyHttpData(String name) throws HttpPostRequestDecoder.NotEnoughDataDecoderException; + + /** + * Initialized the internals from a new chunk + * + * @param content + * the new received chunk + * @throws HttpPostRequestDecoder.ErrorDataDecoderException + * if there is a problem with the charset decoding or other + * errors + */ + InterfaceHttpPostRequestDecoder offer(HttpContent content) + throws HttpPostRequestDecoder.ErrorDataDecoderException; + + /** + * 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 HttpPostRequestDecoder.EndOfDataDecoderException + * No more data will be available + */ + boolean hasNext() throws HttpPostRequestDecoder.EndOfDataDecoderException; + + /** + * 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 HttpPostRequestDecoder.EndOfDataDecoderException + * No more data will be available + */ + InterfaceHttpData next() throws HttpPostRequestDecoder.EndOfDataDecoderException; + + /** + * Destroy the {@link InterfaceHttpPostRequestDecoder} and release all it resources. After this method + * was called it is not possible to operate on it anymore. + */ + void destroy(); + + /** + * Clean all HttpDatas (on Disk) for the current request. + */ + void cleanFiles(); + + /** + * Remove the given FileUpload from the list of FileUploads to clean + */ + void removeHttpDataFromClean(InterfaceHttpData data); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java index e174ac91f4..d698f9145b 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java @@ -53,6 +53,7 @@ public class MemoryAttribute extends AbstractMemoryHttpData implements Attribute throw new NullPointerException("value"); } byte [] bytes = value.getBytes(charset.name()); + checkSize(bytes.length); ByteBuf buffer = wrappedBuffer(bytes); if (definedSize > 0) { definedSize = buffer.readableBytes(); @@ -63,6 +64,7 @@ public class MemoryAttribute extends AbstractMemoryHttpData implements Attribute @Override public void addContent(ByteBuf buffer, boolean last) throws IOException { int localsize = buffer.readableBytes(); + checkSize(size + localsize); if (definedSize > 0 && definedSize < size + localsize) { definedSize = size + localsize; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedAttribute.java index e47e548693..85a72eb7dd 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedAttribute.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedAttribute.java @@ -29,6 +29,7 @@ public class MixedAttribute implements Attribute { private Attribute attribute; private final long limitSize; + protected long maxSize = DefaultHttpDataFactory.MAXSIZE; public MixedAttribute(String name, long limitSize) { this.limitSize = limitSize; @@ -57,12 +58,25 @@ public class MixedAttribute implements Attribute { } } + public void setMaxSize(long maxSize) { + this.maxSize = maxSize; + attribute.setMaxSize(maxSize); + } + + public void checkSize(long newSize) throws IOException { + if (maxSize >= 0 && newSize > maxSize) { + throw new IOException("Size exceed allowed maximum capacity"); + } + } + @Override public void addContent(ByteBuf buffer, boolean last) throws IOException { if (attribute instanceof MemoryAttribute) { + checkSize(attribute.length() + buffer.readableBytes()); if (attribute.length() + buffer.readableBytes() > limitSize) { DiskAttribute diskAttribute = new DiskAttribute(attribute .getName()); + diskAttribute.setMaxSize(maxSize); if (((MemoryAttribute) attribute).getByteBuf() != null) { diskAttribute.addContent(((MemoryAttribute) attribute) .getByteBuf(), false); @@ -130,10 +144,12 @@ public class MixedAttribute implements Attribute { @Override public void setContent(ByteBuf buffer) throws IOException { + checkSize(buffer.readableBytes()); if (buffer.readableBytes() > limitSize) { if (attribute instanceof MemoryAttribute) { // change to Disk attribute = new DiskAttribute(attribute.getName()); + attribute.setMaxSize(maxSize); } } attribute.setContent(buffer); @@ -141,10 +157,12 @@ public class MixedAttribute implements Attribute { @Override public void setContent(File file) throws IOException { + checkSize(file.length()); if (file.length() > limitSize) { if (attribute instanceof MemoryAttribute) { // change to Disk attribute = new DiskAttribute(attribute.getName()); + attribute.setMaxSize(maxSize); } } attribute.setContent(file); @@ -155,6 +173,7 @@ public class MixedAttribute implements Attribute { if (attribute instanceof MemoryAttribute) { // change to Disk even if we don't know the size attribute = new DiskAttribute(attribute.getName()); + attribute.setMaxSize(maxSize); } attribute.setContent(inputStream); } @@ -186,6 +205,9 @@ public class MixedAttribute implements Attribute { @Override public void setValue(String value) throws IOException { + if (value != null) { + checkSize(value.getBytes().length); + } attribute.setValue(value); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedFileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedFileUpload.java index e216810edb..d5f81b04db 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedFileUpload.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedFileUpload.java @@ -32,6 +32,7 @@ public class MixedFileUpload implements FileUpload { private final long limitSize; private final long definedSize; + protected long maxSize = DefaultHttpDataFactory.MAXSIZE; public MixedFileUpload(String name, String filename, String contentType, String contentTransferEncoding, Charset charset, long size, @@ -47,17 +48,29 @@ public class MixedFileUpload implements FileUpload { definedSize = size; } + public void setMaxSize(long maxSize) { + this.maxSize = maxSize; + fileUpload.setMaxSize(maxSize); + } + + public void checkSize(long newSize) throws IOException { + if (maxSize >= 0 && newSize > maxSize) { + throw new IOException("Size exceed allowed maximum capacity"); + } + } + @Override public void addContent(ByteBuf buffer, boolean last) throws IOException { if (fileUpload instanceof MemoryFileUpload) { + checkSize(fileUpload.length() + buffer.readableBytes()); if (fileUpload.length() + buffer.readableBytes() > limitSize) { DiskFileUpload diskFileUpload = new DiskFileUpload(fileUpload .getName(), fileUpload.getFilename(), fileUpload .getContentType(), fileUpload .getContentTransferEncoding(), fileUpload.getCharset(), definedSize); - + diskFileUpload.setMaxSize(maxSize); ByteBuf data = fileUpload.getByteBuf(); if (data != null && data.isReadable()) { diskFileUpload.addContent(data.retain(), false); @@ -143,6 +156,7 @@ public class MixedFileUpload implements FileUpload { @Override public void setContent(ByteBuf buffer) throws IOException { + checkSize(buffer.readableBytes()); if (buffer.readableBytes() > limitSize) { if (fileUpload instanceof MemoryFileUpload) { FileUpload memoryUpload = fileUpload; @@ -152,6 +166,7 @@ public class MixedFileUpload implements FileUpload { .getContentType(), memoryUpload .getContentTransferEncoding(), memoryUpload.getCharset(), definedSize); + fileUpload.setMaxSize(maxSize); // release old upload memoryUpload.release(); @@ -162,6 +177,7 @@ public class MixedFileUpload implements FileUpload { @Override public void setContent(File file) throws IOException { + checkSize(file.length()); if (file.length() > limitSize) { if (fileUpload instanceof MemoryFileUpload) { FileUpload memoryUpload = fileUpload; @@ -172,6 +188,7 @@ public class MixedFileUpload implements FileUpload { .getContentType(), memoryUpload .getContentTransferEncoding(), memoryUpload.getCharset(), definedSize); + fileUpload.setMaxSize(maxSize); // release old upload memoryUpload.release(); @@ -191,6 +208,7 @@ public class MixedFileUpload implements FileUpload { .getContentType(), fileUpload .getContentTransferEncoding(), fileUpload.getCharset(), definedSize); + fileUpload.setMaxSize(maxSize); // release old upload memoryUpload.release(); diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java index 6fbff4aea2..4bd1c374df 100644 --- a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java @@ -27,6 +27,7 @@ import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; @@ -43,7 +44,6 @@ import io.netty.handler.codec.http.multipart.HttpDataFactory; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; 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.IncompatibleDataDecoderException; import io.netty.handler.codec.http.multipart.InterfaceHttpData; import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType; import io.netty.util.CharsetUtil; @@ -142,6 +142,13 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler