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