/* * 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 org.jboss.netty.handler.codec.http.multipart; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.handler.codec.http.HttpChunk; import org.jboss.netty.handler.codec.http.HttpConstants; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadNoBackArrayException; import org.jboss.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize; import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException; import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException; import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.IncompatibleDataDecoderException; import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus; import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException; import org.jboss.netty.util.internal.CaseIgnoringComparator; 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; /** * This decoder will decode Body and can handle standard (non multipart) POST BODY. */ @SuppressWarnings({ "deprecation", "RedundantThrowsDeclaration" }) 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<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 ChannelBuffer undecodedChunk; /** * Body HttpDatas current position */ private int bodyListHttpDataRank; /** * Current status */ private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED; /** * The current Attribute that is currently in decode process */ private Attribute currentAttribute; /** * * @param request 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 HttpPostStandardRequestDecoder(HttpRequest request) throws ErrorDataDecoderException, IncompatibleDataDecoderException { 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 IncompatibleDataDecoderException if the request has no body to decode * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors */ public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request) throws ErrorDataDecoderException, IncompatibleDataDecoderException { 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 IncompatibleDataDecoderException if the request has no body to decode * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors */ public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) throws ErrorDataDecoderException, IncompatibleDataDecoderException { 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 (!this.request.isChunked()) { undecodedChunk = this.request.getContent(); isLastChunk = true; parseBody(); } } public boolean isMultipart() { return false; } public List<InterfaceHttpData> getBodyHttpDatas() throws NotEnoughDataDecoderException { if (!isLastChunk) { throw new NotEnoughDataDecoderException(); } return bodyListHttpData; } public List<InterfaceHttpData> getBodyHttpDatas(String name) throws NotEnoughDataDecoderException { if (!isLastChunk) { throw new NotEnoughDataDecoderException(); } return bodyMapHttpData.get(name); } public InterfaceHttpData getBodyHttpData(String name) throws NotEnoughDataDecoderException { if (!isLastChunk) { throw new NotEnoughDataDecoderException(); } List<InterfaceHttpData> list = bodyMapHttpData.get(name); if (list != null) { return list.get(0); } return null; } public void offer(HttpChunk chunk) throws ErrorDataDecoderException { ChannelBuffer chunked = chunk.getContent(); if (undecodedChunk == null) { undecodedChunk = chunked; } else { //undecodedChunk = ChannelBuffers.wrappedBuffer(undecodedChunk, chunk.getContent()); // less memory usage undecodedChunk = ChannelBuffers.wrappedBuffer( undecodedChunk, chunked); } if (chunk.isLast()) { isLastChunk = true; } parseBody(); } public boolean hasNext() throws EndOfDataDecoderException { if (currentStatus == MultiPartStatus.EPILOGUE) { // OK except if end of list if (bodyListHttpDataRank >= bodyListHttpData.size()) { throw new EndOfDataDecoderException(); } } return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size(); } public InterfaceHttpData next() throws EndOfDataDecoderException { if (hasNext()) { return bodyListHttpData.get(bodyListHttpDataRank++); } return null; } /** * This method 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 */ private 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); } /** * This method 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.readable() && 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.slice(firstpos, ampersandpos - firstpos)); firstpos = currentpos; contRead = true; } else if (read == HttpConstants.CR) { if (undecodedChunk.readable()) { read = (char) undecodedChunk.readUnsignedByte(); currentpos++; if (read == HttpConstants.LF) { currentStatus = MultiPartStatus.PREEPILOGUE; ampersandpos = currentpos - 2; setFinalBuffer( undecodedChunk.slice(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.slice(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.slice(firstpos, ampersandpos - firstpos)); } else if (! currentAttribute.isCompleted()) { setFinalBuffer(ChannelBuffers.EMPTY_BUFFER); } firstpos = currentpos; currentStatus = MultiPartStatus.EPILOGUE; undecodedChunk.readerIndex(firstpos); return; } if (contRead && currentAttribute != null) { // reset index except if to continue in case of FIELD status if (currentStatus == MultiPartStatus.FIELD) { currentAttribute.addContent( undecodedChunk.slice(firstpos, currentpos - firstpos), false); firstpos = currentpos; } undecodedChunk.readerIndex(firstpos); } else { // end of line or end of block so keep index to last valid position undecodedChunk.readerIndex(firstpos); } } 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 method 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.slice(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.slice(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.slice(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.slice(firstpos, ampersandpos - firstpos)); } else if (! currentAttribute.isCompleted()) { setFinalBuffer(ChannelBuffers.EMPTY_BUFFER); } firstpos = currentpos; currentStatus = MultiPartStatus.EPILOGUE; undecodedChunk.readerIndex(firstpos); return; } if (contRead && currentAttribute != null) { // reset index except if to continue in case of FIELD status if (currentStatus == MultiPartStatus.FIELD) { currentAttribute.addContent( undecodedChunk.slice(firstpos, currentpos - firstpos), false); firstpos = currentpos; } undecodedChunk.readerIndex(firstpos); } else { // end of line or end of block so keep index to last valid position undecodedChunk.readerIndex(firstpos); } } 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(ChannelBuffer buffer) throws ErrorDataDecoderException, IOException { currentAttribute.addContent(buffer, true); String value = decodeAttribute( currentAttribute.getChannelBuffer().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); } } public void cleanFiles() { factory.cleanRequestHttpDatas(request); } public void removeHttpDataFromClean(InterfaceHttpData data) { factory.removeHttpDataFromClean(request, data); } }