All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.netty.handler.codec.http.HttpPostRequestDecoder Maven / Gradle / Ivy

/*
 * Copyright 2011 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;

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 io.netty.buffer.ChannelBuffer;
import io.netty.buffer.ChannelBuffers;
import io.netty.handler.codec.http.HttpPostBodyUtil.TransferEncodingMechanism;

/**
 * This decoder will decode Body and can handle POST BODY.
 */
public class HttpPostRequestDecoder {
    /**
     * 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 ChannelBuffer 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 status
     */
    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;

    /**
    *
    * @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 HttpPostRequestDecoder(HttpRequest request)
            throws ErrorDataDecoderException, IncompatibleDataDecoderException {
        this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
                request, HttpCodecUtil.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 HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request)
            throws ErrorDataDecoderException, IncompatibleDataDecoderException {
        this(factory, request, HttpCodecUtil.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 HttpPostRequestDecoder(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;
        HttpMethod method = request.getMethod();
        if (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT)) {
            bodyToDecode = true;
        }
        this.charset = charset;
        this.factory = factory;
        // Fill default values
        if (this.request.containsHeader(HttpHeaders.Names.CONTENT_TYPE)) {
            checkMultipart(this.request.getHeader(HttpHeaders.Names.CONTENT_TYPE));
        } else {
            isMultipart = false;
        }
        if (!bodyToDecode) {
            throw new IncompatibleDataDecoderException("No Body to decode");
        }
        if (!this.request.isChunked()) {
            undecodedChunk = this.request.getContent();
            isLastChunk = true;
            parseBody();
        }
    }

    /**
     * states follow
     * NOTSTARTED PREAMBLE (
     *  (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*
     *  (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE
     *     (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+
     *   MIXEDCLOSEDELIMITER)*
     * CLOSEDELIMITER)+ EPILOGUE
     *
     *  First status is: NOSTARTED

        Content-type: multipart/form-data, boundary=AaB03x     => PREAMBLE in Header

        --AaB03x                                               => HEADERDELIMITER
        content-disposition: form-data; name="field1"          => DISPOSITION

        Joe Blow                                               => FIELD
        --AaB03x                                               => HEADERDELIMITER
        content-disposition: form-data; name="pics"            => DISPOSITION
        Content-type: multipart/mixed, boundary=BbC04y

        --BbC04y                                               => MIXEDDELIMITER
        Content-disposition: attachment; filename="file1.txt"  => MIXEDDISPOSITION
        Content-Type: text/plain

        ... contents of file1.txt ...                          => MIXEDFILEUPLOAD
        --BbC04y                                               => MIXEDDELIMITER
        Content-disposition: file; filename="file2.gif"  => MIXEDDISPOSITION
        Content-type: image/gif
        Content-Transfer-Encoding: binary

          ...contents of file2.gif...                          => MIXEDFILEUPLOAD
        --BbC04y--                                             => MIXEDCLOSEDELIMITER
        --AaB03x--                                             => CLOSEDELIMITER

       Once CLOSEDELIMITER is found, last status is EPILOGUE
     */
    private 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.
     * @param contentType
     * @throws ErrorDataDecoderException
     */
    private void checkMultipart(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)) {
            String[] boundary = headerContentType[1].split("=");
            if (boundary.length != 2) {
                throw new ErrorDataDecoderException("Needs a boundary value");
            }
            multipartDataBoundary = "--" + boundary[1];
            isMultipart = true;
            currentStatus = MultiPartStatus.HEADERDELIMITER;
        } else {
            isMultipart = false;
        }
    }

    /**
     * True if this request is a Multipart request
     * @return True if this request is a Multipart request
     */
    public boolean isMultipart() {
        return isMultipart;
    }

    /**
     * This method returns a List of all HttpDatas from body.
* * If chunked, all chunks must have been offered using offer() method. * If not, NotEnoughDataDecoderException will be raised. * * @return the list of HttpDatas from Body part for POST method * @throws NotEnoughDataDecoderException Need more chunks */ public List getBodyHttpDatas() throws NotEnoughDataDecoderException { if (!isLastChunk) { throw new NotEnoughDataDecoderException(); } return bodyListHttpData; } /** * This method returns a List of all HttpDatas with the given name from body.
* * If chunked, all chunks must have been offered using offer() method. * If not, NotEnoughDataDecoderException will be raised. * @param name * @return All Body HttpDatas with the given name (ignore case) * @throws NotEnoughDataDecoderException need more chunks */ public List getBodyHttpDatas(String name) throws NotEnoughDataDecoderException { if (!isLastChunk) { throw new NotEnoughDataDecoderException(); } return bodyMapHttpData.get(name); } /** * This method returns the first InterfaceHttpData with the given name from body.
* * If chunked, all chunks must have been offered using offer() method. * If not, NotEnoughDataDecoderException will be raised. * * @param name * @return The first Body InterfaceHttpData with the given name (ignore case) * @throws NotEnoughDataDecoderException need more chunks */ public InterfaceHttpData getBodyHttpData(String name) throws NotEnoughDataDecoderException { 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 chunk the new received chunk * @throws ErrorDataDecoderException if there is a problem with the charset decoding or * other errors */ 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(); } /** * True if at current status, there is an available decoded InterfaceHttpData from the Body. * * This method works for chunked and not chunked request. * * @return True if at current status, there is a decoded InterfaceHttpData * @throws EndOfDataDecoderException No more data will be available */ public boolean hasNext() throws EndOfDataDecoderException { if (currentStatus == MultiPartStatus.EPILOGUE) { // OK except if end of list if (bodyListHttpDataRank >= bodyListHttpData.size()) { throw new EndOfDataDecoderException(); } } return bodyListHttpData.size() > 0 && 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. * * @return the next available InterfaceHttpData or null if none * @throws EndOfDataDecoderException No more data will be available */ 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; } if (isMultipart) { parseBodyMultipart(); } else { parseBodyAttributes(); } } /** * Utility function to add a new decoded data * @param data */ private 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 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 { int firstpos = undecodedChunk.readerIndex(); int currentpos = firstpos; int equalpos = firstpos; int ampersandpos = firstpos; 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 == HttpCodecUtil.CR) { if (undecodedChunk.readable()) { read = (char) undecodedChunk.readUnsignedByte(); currentpos++; if (read == HttpCodecUtil.LF) { currentStatus = MultiPartStatus.PREEPILOGUE; ampersandpos = currentpos - 2; setFinalBuffer( undecodedChunk.slice(firstpos, ampersandpos - firstpos)); firstpos = currentpos; contRead = false; } else { // Error contRead = false; throw new ErrorDataDecoderException("Bad end of line"); } } else { currentpos--; } } else if (read == HttpCodecUtil.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; 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 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(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 * @param s * @param charset * @return the decoded component * @throws ErrorDataDecoderException */ 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); } } /** * 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 * * @param state * @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 status"); case PREAMBLE: // Content-type: multipart/form-data, boundary=AaB03x throw new ErrorDataDecoderException( "Should not be called with the current status"); 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, 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."); } } /** * Find the next Multipart Delimiter * @param delimiter delimiter to find * @param dispositionStatus the next status if the delimiter is a start * @param closeDelimiterStatus the next status 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(); HttpPostBodyUtil.skipControlCharacters(undecodedChunk); skipOneLine(); String newline; try { newline = readLine(); } catch (NotEnoughDataDecoderException e) { undecodedChunk.readerIndex(readerIndex); return null; } if (newline.equals(delimiter)) { currentStatus = dispositionStatus; return decodeMultipart(dispositionStatus); } else 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()) { HttpPostBodyUtil.skipControlCharacters(undecodedChunk); String newline; try { newline = readLine(); } catch (NotEnoughDataDecoderException e) { undecodedChunk.readerIndex(readerIndex); return null; } String[] contents = splitMultipartHeader(newline); if (contents[0].equalsIgnoreCase(HttpPostBodyUtil.CONTENT_DISPOSITION)) { boolean checkSecondArg = false; 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 = contents[i].split("="); Attribute attribute; try { attribute = factory.createAttribute(request, values[0].trim(), decodeAttribute(cleanString(values[1]), charset)); } 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 = contents[2].split("="); 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 = contents[i].split("="); 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, contents[0].trim(), decodeAttribute(cleanString(contents[i]), charset)); } 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 */ private 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, nameAttribute.getValue(), 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; } /** * Clean all HttpDatas (on Disk) for the current request. */ public void cleanFiles() { factory.cleanRequestHttpDatas(request); } /** * Remove the given FileUpload from the list of FileUploads to clean */ public void removeHttpDataFromClean(InterfaceHttpData data) { 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 readLine() throws NotEnoughDataDecoderException { int readerIndex = undecodedChunk.readerIndex(); try { StringBuilder sb = new StringBuilder(64); while (undecodedChunk.readable()) { byte nextByte = undecodedChunk.readByte(); if (nextByte == HttpCodecUtil.CR) { nextByte = undecodedChunk.readByte(); if (nextByte == HttpCodecUtil.LF) { return sb.toString(); } } else if (nextByte == HttpCodecUtil.LF) { return sb.toString(); } else { sb.append((char) nextByte); } } } 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. * @param delimiter * @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 { int readerIndex = undecodedChunk.readerIndex(); // found the decoder limit boolean newLine = true; int index = 0; int lastPosition = undecodedChunk.readerIndex(); boolean found = false; while (undecodedChunk.readable()) { 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 == HttpCodecUtil.CR) { if (undecodedChunk.readable()) { nextByte = undecodedChunk.readByte(); if (nextByte == HttpCodecUtil.LF) { newLine = true; index = 0; lastPosition = undecodedChunk.readerIndex() - 2; } } } else if (nextByte == HttpCodecUtil.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 == HttpCodecUtil.CR) { if (undecodedChunk.readable()) { nextByte = undecodedChunk.readByte(); if (nextByte == HttpCodecUtil.LF) { newLine = true; index = 0; lastPosition = undecodedChunk.readerIndex() - 2; } } } else if (nextByte == HttpCodecUtil.LF) { newLine = true; index = 0; lastPosition = undecodedChunk.readerIndex() - 1; } else { // save last valid position lastPosition = undecodedChunk.readerIndex(); } } } ChannelBuffer buffer = undecodedChunk.slice(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 loadFieldMultipart(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.readable()) { 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 == HttpCodecUtil.CR) { if (undecodedChunk.readable()) { nextByte = undecodedChunk.readByte(); if (nextByte == HttpCodecUtil.LF) { newLine = true; index = 0; lastPosition = undecodedChunk.readerIndex() - 2; } } } else if (nextByte == HttpCodecUtil.LF) { newLine = true; index = 0; lastPosition = undecodedChunk.readerIndex() - 1; } else { lastPosition = undecodedChunk.readerIndex(); } } } else { // continue until end of line if (nextByte == HttpCodecUtil.CR) { if (undecodedChunk.readable()) { nextByte = undecodedChunk.readByte(); if (nextByte == HttpCodecUtil.LF) { newLine = true; index = 0; lastPosition = undecodedChunk.readerIndex() - 2; } } } else if (nextByte == HttpCodecUtil.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.slice(readerIndex, lastPosition - readerIndex), true); } catch (IOException e) { throw new ErrorDataDecoderException(e); } undecodedChunk.readerIndex(lastPosition); } else { try { currentAttribute.addContent( undecodedChunk.slice(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 String cleanString(String field) { StringBuilder sb = new StringBuilder(field.length()); int i = 0; for (i = 0; i < field.length(); i ++) { char nextChar = field.charAt(i); if (nextChar == HttpCodecUtil.COLON) { sb.append(HttpCodecUtil.SP); } else if (nextChar == HttpCodecUtil.COMMA) { sb.append(HttpCodecUtil.SP); } else if (nextChar == HttpCodecUtil.EQUALS) { sb.append(HttpCodecUtil.SP); } else if (nextChar == HttpCodecUtil.SEMICOLON) { sb.append(HttpCodecUtil.SP); } else if (nextChar == HttpCodecUtil.HT) { sb.append(HttpCodecUtil.SP); } else if (nextChar == HttpCodecUtil.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.readable()) { return false; } byte nextByte = undecodedChunk.readByte(); if (nextByte == HttpCodecUtil.CR) { if (!undecodedChunk.readable()) { undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1); return false; } nextByte = undecodedChunk.readByte(); if (nextByte == HttpCodecUtil.LF) { return true; } undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 2); return false; } else if (nextByte == HttpCodecUtil.LF) { return true; } undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1); return false; } /** * Split the very first line (Content-Type value) in 2 Strings * @param sb * @return the array of 2 Strings */ private String[] splitHeaderContentType(String sb) { int size = sb.length(); int aStart; int aEnd; int bStart; int bEnd; aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0); aEnd = HttpPostBodyUtil.findWhitespace(sb, aStart); if (aEnd >= size) { return new String[] { sb, "" }; } if (sb.charAt(aEnd) == ';') { aEnd --; } bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd); bEnd = HttpPostBodyUtil.findEndOfString(sb); return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd) }; } /** * Split one header in Multipart * @param sb * @return an array of String where rank 0 is the name of the header, follows by several * values that were separated by ';' or ',' */ private 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 = null; if (svalue.indexOf(";") >= 0) { values = svalue.split(";"); } 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; } /** * Exception when try reading data from request in chunked format, and not enough * data are available (need more chunks) */ public static class NotEnoughDataDecoderException extends Exception { /** */ private static final long serialVersionUID = -7846841864603865638L; /** */ public NotEnoughDataDecoderException() { } /** * @param arg0 */ public NotEnoughDataDecoderException(String arg0) { super(arg0); } /** * @param arg0 */ public NotEnoughDataDecoderException(Throwable arg0) { super(arg0); } /** * @param arg0 * @param arg1 */ public NotEnoughDataDecoderException(String arg0, Throwable arg1) { super(arg0, arg1); } } /** * Exception when the body is fully decoded, even if there is still data */ public static class EndOfDataDecoderException extends Exception { /** */ private static final long serialVersionUID = 1336267941020800769L; } /** * Exception when an error occurs while decoding */ public static class ErrorDataDecoderException extends Exception { /** */ private static final long serialVersionUID = 5020247425493164465L; /** */ public ErrorDataDecoderException() { } /** * @param arg0 */ public ErrorDataDecoderException(String arg0) { super(arg0); } /** * @param arg0 */ public ErrorDataDecoderException(Throwable arg0) { super(arg0); } /** * @param arg0 * @param arg1 */ public ErrorDataDecoderException(String arg0, Throwable arg1) { super(arg0, arg1); } } /** * Exception when an unappropriated method was called on a request */ public static class IncompatibleDataDecoderException extends Exception { /** */ private static final long serialVersionUID = -953268047926250267L; /** */ public IncompatibleDataDecoderException() { } /** * @param arg0 */ public IncompatibleDataDecoderException(String arg0) { super(arg0); } /** * @param arg0 */ public IncompatibleDataDecoderException(Throwable arg0) { super(arg0); } /** * @param arg0 * @param arg1 */ public IncompatibleDataDecoderException(String arg0, Throwable arg1) { super(arg0, arg1); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy