View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http.multipart;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.buffer.ByteBufAllocator;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.handler.codec.DecoderResult;
22  import io.netty.handler.codec.http.DefaultFullHttpRequest;
23  import io.netty.handler.codec.http.DefaultHttpContent;
24  import io.netty.handler.codec.http.EmptyHttpHeaders;
25  import io.netty.handler.codec.http.FullHttpRequest;
26  import io.netty.handler.codec.http.HttpConstants;
27  import io.netty.handler.codec.http.HttpContent;
28  import io.netty.handler.codec.http.HttpHeaderNames;
29  import io.netty.handler.codec.http.HttpHeaderValues;
30  import io.netty.handler.codec.http.HttpHeaders;
31  import io.netty.handler.codec.http.HttpMethod;
32  import io.netty.handler.codec.http.HttpRequest;
33  import io.netty.handler.codec.http.HttpUtil;
34  import io.netty.handler.codec.http.HttpVersion;
35  import io.netty.handler.codec.http.LastHttpContent;
36  import io.netty.handler.stream.ChunkedInput;
37  import io.netty.util.internal.ObjectUtil;
38  import io.netty.util.internal.StringUtil;
39  
40  import java.io.File;
41  import java.io.IOException;
42  import java.io.UnsupportedEncodingException;
43  import java.net.URLEncoder;
44  import java.nio.charset.Charset;
45  import java.util.ArrayList;
46  import java.util.List;
47  import java.util.ListIterator;
48  import java.util.concurrent.ThreadLocalRandom;
49  
50  import static io.netty.buffer.Unpooled.wrappedBuffer;
51  import static io.netty.util.internal.ObjectUtil.checkNotNull;
52  
53  /**
54   * This encoder will help to encode Request for a FORM as POST.
55   *
56   * <P>According to RFC 7231, POST, PUT and OPTIONS allow to have a body.
57   * This encoder will support widely all methods except TRACE since the RFC notes
58   * for GET, DELETE, HEAD and CONNECT: (replaces XXX by one of these methods)</P>
59   * <P>"A payload within a XXX request message has no defined semantics;
60   * sending a payload body on a XXX request might cause some existing
61   * implementations to reject the request."</P>
62   * <P>On the contrary, for TRACE method, RFC says:</P>
63   * <P>"A client MUST NOT send a message body in a TRACE request."</P>
64   */
65  public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
66  
67      /**
68       * Different modes to use to encode form data.
69       */
70      public enum EncoderMode {
71          /**
72           * Legacy mode which should work for most. It is known to not work with OAUTH. For OAUTH use
73           * {@link EncoderMode#RFC3986}. The W3C form recommendations this for submitting post form data.
74           */
75          RFC1738,
76  
77          /**
78           * Mode which is more new and is used for OAUTH
79           */
80          RFC3986,
81  
82          /**
83           * The HTML5 spec disallows mixed mode in multipart/form-data
84           * requests. More concretely this means that more files submitted
85           * under the same name will not be encoded using mixed mode, but
86           * will be treated as distinct fields.
87           *
88           * Reference:
89           *   https://www.w3.org/TR/html5/forms.html#multipart-form-data
90           */
91          HTML5
92      }
93  
94      private static final String ASTERISK = "*";
95      private static final String PLUS = "+";
96      private static final String TILDE = "~";
97      private static final String ASTERISK_REPLACEMENT = "%2A";
98      private static final String PLUS_REPLACEMENT = "%20";
99      private static final String TILDE_REPLACEMENT = "%7E";
100 
101     /**
102      * Factory used to create InterfaceHttpData
103      */
104     private final HttpDataFactory factory;
105 
106     /**
107      * Request to encode
108      */
109     private final HttpRequest request;
110 
111     /**
112      * Default charset to use
113      */
114     private final Charset charset;
115 
116     /**
117      * Chunked false by default
118      */
119     private boolean isChunked;
120 
121     /**
122      * InterfaceHttpData for Body (without encoding)
123      */
124     private final List<InterfaceHttpData> bodyListDatas;
125     /**
126      * The final Multipart List of InterfaceHttpData including encoding
127      */
128     final List<InterfaceHttpData> multipartHttpDatas;
129 
130     /**
131      * Does this request is a Multipart request
132      */
133     private final boolean isMultipart;
134 
135     /**
136      * If multipart, this is the boundary for the flobal multipart
137      */
138     String multipartDataBoundary;
139 
140     /**
141      * If multipart, there could be internal multiparts (mixed) to the global multipart. Only one level is allowed.
142      */
143     String multipartMixedBoundary;
144     /**
145      * To check if the header has been finalized
146      */
147     private boolean headerFinalized;
148 
149     private final EncoderMode encoderMode;
150 
151     /**
152      *
153      * @param request
154      *            the request to encode
155      * @param multipart
156      *            True if the FORM is a ENCTYPE="multipart/form-data"
157      * @throws NullPointerException
158      *             for request
159      * @throws ErrorDataEncoderException
160      *             if the request is a TRACE
161      */
162     public HttpPostRequestEncoder(HttpRequest request, boolean multipart) throws ErrorDataEncoderException {
163         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, multipart,
164                 HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738);
165     }
166 
167     /**
168      *
169      * @param factory
170      *            the factory used to create InterfaceHttpData
171      * @param request
172      *            the request to encode
173      * @param multipart
174      *            True if the FORM is a ENCTYPE="multipart/form-data"
175      * @throws NullPointerException
176      *             for request and factory
177      * @throws ErrorDataEncoderException
178      *             if the request is a TRACE
179      */
180     public HttpPostRequestEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart)
181             throws ErrorDataEncoderException {
182         this(factory, request, multipart, HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738);
183     }
184 
185     /**
186      *
187      * @param factory
188      *            the factory used to create InterfaceHttpData
189      * @param request
190      *            the request to encode
191      * @param multipart
192      *            True if the FORM is a ENCTYPE="multipart/form-data"
193      * @param charset
194      *            the charset to use as default
195      * @param encoderMode
196      *            the mode for the encoder to use. See {@link EncoderMode} for the details.
197      * @throws NullPointerException
198      *             for request or charset or factory
199      * @throws ErrorDataEncoderException
200      *             if the request is a TRACE
201      */
202     public HttpPostRequestEncoder(
203             HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset,
204             EncoderMode encoderMode)
205             throws ErrorDataEncoderException {
206         this.request = checkNotNull(request, "request");
207         this.charset = checkNotNull(charset, "charset");
208         this.factory = checkNotNull(factory, "factory");
209         if (HttpMethod.TRACE.equals(request.method())) {
210             throw new ErrorDataEncoderException("Cannot create a Encoder if request is a TRACE");
211         }
212         // Fill default values
213         bodyListDatas = new ArrayList<InterfaceHttpData>();
214         // default mode
215         isLastChunk = false;
216         isLastChunkSent = false;
217         isMultipart = multipart;
218         multipartHttpDatas = new ArrayList<InterfaceHttpData>();
219         this.encoderMode = encoderMode;
220         if (isMultipart) {
221             initDataMultipart();
222         }
223     }
224 
225     /**
226      * Clean all HttpDatas (on Disk) for the current request.
227      */
228     public void cleanFiles() {
229         factory.cleanRequestHttpData(request);
230     }
231 
232     /**
233      * Does the last non empty chunk already encoded so that next chunk will be empty (last chunk)
234      */
235     private boolean isLastChunk;
236     /**
237      * Last chunk already sent
238      */
239     private boolean isLastChunkSent;
240     /**
241      * The current FileUpload that is currently in encode process
242      */
243     private FileUpload currentFileUpload;
244     /**
245      * While adding a FileUpload, is the multipart currently in Mixed Mode
246      */
247     private boolean duringMixedMode;
248     /**
249      * Global Body size
250      */
251     private long globalBodySize;
252     /**
253      * Global Transfer progress
254      */
255     private long globalProgress;
256 
257     /**
258      * True if this request is a Multipart request
259      *
260      * @return True if this request is a Multipart request
261      */
262     public boolean isMultipart() {
263         return isMultipart;
264     }
265 
266     /**
267      * Init the delimiter for Global Part (Data).
268      */
269     private void initDataMultipart() {
270         multipartDataBoundary = getNewMultipartDelimiter();
271     }
272 
273     /**
274      * Init the delimiter for Mixed Part (Mixed).
275      */
276     private void initMixedMultipart() {
277         multipartMixedBoundary = getNewMultipartDelimiter();
278     }
279 
280     /**
281      *
282      * @return a newly generated Delimiter (either for DATA or MIXED)
283      */
284     private static String getNewMultipartDelimiter() {
285         // construct a generated delimiter
286         return Long.toHexString(ThreadLocalRandom.current().nextLong());
287     }
288 
289     /**
290      * This getMethod returns a List of all InterfaceHttpData from body part.<br>
291 
292      * @return the list of InterfaceHttpData from Body part
293      */
294     public List<InterfaceHttpData> getBodyListAttributes() {
295         return bodyListDatas;
296     }
297 
298     /**
299      * Set the Body HttpDatas list
300      *
301      * @throws NullPointerException
302      *             for datas
303      * @throws ErrorDataEncoderException
304      *             if the encoding is in error or if the finalize were already done
305      */
306     public void setBodyHttpDatas(List<InterfaceHttpData> datas) throws ErrorDataEncoderException {
307         ObjectUtil.checkNotNull(datas, "datas");
308         globalBodySize = 0;
309         bodyListDatas.clear();
310         currentFileUpload = null;
311         duringMixedMode = false;
312         multipartHttpDatas.clear();
313         for (InterfaceHttpData data : datas) {
314             addBodyHttpData(data);
315         }
316     }
317 
318     /**
319      * Add a simple attribute in the body as Name=Value
320      *
321      * @param name
322      *            name of the parameter
323      * @param value
324      *            the value of the parameter
325      * @throws NullPointerException
326      *             for name
327      * @throws ErrorDataEncoderException
328      *             if the encoding is in error or if the finalize were already done
329      */
330     public void addBodyAttribute(String name, String value) throws ErrorDataEncoderException {
331         String svalue = value != null? value : StringUtil.EMPTY_STRING;
332         Attribute data = factory.createAttribute(request, checkNotNull(name, "name"), svalue);
333         addBodyHttpData(data);
334     }
335 
336     /**
337      * Add a file as a FileUpload
338      *
339      * @param name
340      *            the name of the parameter
341      * @param file
342      *            the file to be uploaded (if not Multipart mode, only the filename will be included)
343      * @param contentType
344      *            the associated contentType for the File
345      * @param isText
346      *            True if this file should be transmitted in Text format (else binary)
347      * @throws NullPointerException
348      *             for name and file
349      * @throws ErrorDataEncoderException
350      *             if the encoding is in error or if the finalize were already done
351      */
352     public void addBodyFileUpload(String name, File file, String contentType, boolean isText)
353             throws ErrorDataEncoderException {
354         addBodyFileUpload(name, file.getName(), file, contentType, isText);
355     }
356 
357     /**
358      * Add a file as a FileUpload
359      *
360      * @param name
361      *            the name of the parameter
362      * @param file
363      *            the file to be uploaded (if not Multipart mode, only the filename will be included)
364      * @param filename
365      *            the filename to use for this File part, empty String will be ignored by
366      *            the encoder
367      * @param contentType
368      *            the associated contentType for the File
369      * @param isText
370      *            True if this file should be transmitted in Text format (else binary)
371      * @throws NullPointerException
372      *             for name and file
373      * @throws ErrorDataEncoderException
374      *             if the encoding is in error or if the finalize were already done
375      */
376     public void addBodyFileUpload(String name, String filename, File file, String contentType, boolean isText)
377             throws ErrorDataEncoderException {
378         checkNotNull(name, "name");
379         checkNotNull(file, "file");
380         if (filename == null) {
381             filename = StringUtil.EMPTY_STRING;
382         }
383         String scontentType = contentType;
384         String contentTransferEncoding = null;
385         if (contentType == null) {
386             if (isText) {
387                 scontentType = HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE;
388             } else {
389                 scontentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
390             }
391         }
392         if (!isText) {
393             contentTransferEncoding = HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value();
394         }
395         FileUpload fileUpload = factory.createFileUpload(request, name, filename, scontentType,
396                 contentTransferEncoding, null, file.length());
397         try {
398             fileUpload.setContent(file);
399         } catch (IOException e) {
400             throw new ErrorDataEncoderException(e);
401         }
402         addBodyHttpData(fileUpload);
403     }
404 
405     /**
406      * Add a series of Files associated with one File parameter
407      *
408      * @param name
409      *            the name of the parameter
410      * @param file
411      *            the array of files
412      * @param contentType
413      *            the array of content Types associated with each file
414      * @param isText
415      *            the array of isText attribute (False meaning binary mode) for each file
416      * @throws IllegalArgumentException
417      *             also throws if array have different sizes
418      * @throws ErrorDataEncoderException
419      *             if the encoding is in error or if the finalize were already done
420      */
421     public void addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText)
422             throws ErrorDataEncoderException {
423         if (file.length != contentType.length && file.length != isText.length) {
424             throw new IllegalArgumentException("Different array length");
425         }
426         for (int i = 0; i < file.length; i++) {
427             addBodyFileUpload(name, file[i], contentType[i], isText[i]);
428         }
429     }
430 
431     /**
432      * Add the InterfaceHttpData to the Body list
433      *
434      * @throws NullPointerException
435      *             for data
436      * @throws ErrorDataEncoderException
437      *             if the encoding is in error or if the finalize were already done
438      */
439     public void addBodyHttpData(InterfaceHttpData data) throws ErrorDataEncoderException {
440         if (headerFinalized) {
441             throw new ErrorDataEncoderException("Cannot add value once finalized");
442         }
443         bodyListDatas.add(checkNotNull(data, "data"));
444         if (!isMultipart) {
445             if (data instanceof Attribute) {
446                 Attribute attribute = (Attribute) data;
447                 try {
448                     // name=value& with encoded name and attribute
449                     String key = encodeAttribute(attribute.getName(), charset);
450                     String value = encodeAttribute(attribute.getValue(), charset);
451                     Attribute newattribute = factory.createAttribute(request, key, value);
452                     multipartHttpDatas.add(newattribute);
453                     globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
454                 } catch (IOException e) {
455                     throw new ErrorDataEncoderException(e);
456                 }
457             } else if (data instanceof FileUpload) {
458                 // since not Multipart, only name=filename => Attribute
459                 FileUpload fileUpload = (FileUpload) data;
460                 // name=filename& with encoded name and filename
461                 String key = encodeAttribute(fileUpload.getName(), charset);
462                 String value = encodeAttribute(fileUpload.getFilename(), charset);
463                 Attribute newattribute = factory.createAttribute(request, key, value);
464                 multipartHttpDatas.add(newattribute);
465                 globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
466             }
467             return;
468         }
469         /*
470          * Logic:
471          * if not Attribute:
472          *      add Data to body list
473          *      if (duringMixedMode)
474          *          add endmixedmultipart delimiter
475          *          currentFileUpload = null
476          *          duringMixedMode = false;
477          *      add multipart delimiter, multipart body header and Data to multipart list
478          *      reset currentFileUpload, duringMixedMode
479          * if FileUpload: take care of multiple file for one field => mixed mode
480          *      if (duringMixedMode)
481          *          if (currentFileUpload.name == data.name)
482          *              add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
483          *          else
484          *              add endmixedmultipart delimiter, multipart body header and Data to multipart list
485          *              currentFileUpload = data
486          *              duringMixedMode = false;
487          *      else
488          *          if (currentFileUpload.name == data.name)
489          *              change multipart body header of previous file into multipart list to
490          *                      mixedmultipart start, mixedmultipart body header
491          *              add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
492          *              duringMixedMode = true
493          *          else
494          *              add multipart delimiter, multipart body header and Data to multipart list
495          *              currentFileUpload = data
496          *              duringMixedMode = false;
497          * Do not add last delimiter! Could be:
498          * if duringmixedmode: endmixedmultipart + endmultipart
499          * else only endmultipart
500          */
501         if (data instanceof Attribute) {
502             if (duringMixedMode) {
503                 InternalAttribute internal = new InternalAttribute(charset);
504                 internal.addValue("\r\n--" + multipartMixedBoundary + "--");
505                 multipartHttpDatas.add(internal);
506                 multipartMixedBoundary = null;
507                 currentFileUpload = null;
508                 duringMixedMode = false;
509             }
510             InternalAttribute internal = new InternalAttribute(charset);
511             if (!multipartHttpDatas.isEmpty()) {
512                 // previously a data field so CRLF
513                 internal.addValue("\r\n");
514             }
515             internal.addValue("--" + multipartDataBoundary + "\r\n");
516             // content-disposition: form-data; name="field1"
517             Attribute attribute = (Attribute) data;
518             internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
519                     + HttpHeaderValues.NAME + "=\"" + attribute.getName() + "\"\r\n");
520             // Add Content-Length: xxx
521             internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
522                     attribute.length() + "\r\n");
523             Charset localcharset = attribute.getCharset();
524             if (localcharset != null) {
525                 // Content-Type: text/plain; charset=charset
526                 internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " +
527                         HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE + "; " +
528                         HttpHeaderValues.CHARSET + '='
529                         + localcharset.name() + "\r\n");
530             }
531             // CRLF between body header and data
532             internal.addValue("\r\n");
533             multipartHttpDatas.add(internal);
534             multipartHttpDatas.add(data);
535             globalBodySize += attribute.length() + internal.size();
536         } else if (data instanceof FileUpload) {
537             FileUpload fileUpload = (FileUpload) data;
538             InternalAttribute internal = new InternalAttribute(charset);
539             if (!multipartHttpDatas.isEmpty()) {
540                 // previously a data field so CRLF
541                 internal.addValue("\r\n");
542             }
543             boolean localMixed;
544             if (duringMixedMode) {
545                 if (currentFileUpload != null && currentFileUpload.getName().equals(fileUpload.getName())) {
546                     // continue a mixed mode
547 
548                     localMixed = true;
549                 } else {
550                     // end a mixed mode
551 
552                     // add endmixedmultipart delimiter, multipart body header
553                     // and
554                     // Data to multipart list
555                     internal.addValue("--" + multipartMixedBoundary + "--");
556                     multipartHttpDatas.add(internal);
557                     multipartMixedBoundary = null;
558                     // start a new one (could be replaced if mixed start again
559                     // from here
560                     internal = new InternalAttribute(charset);
561                     internal.addValue("\r\n");
562                     localMixed = false;
563                     // new currentFileUpload and no more in Mixed mode
564                     currentFileUpload = fileUpload;
565                     duringMixedMode = false;
566                 }
567             } else {
568                 if (encoderMode != EncoderMode.HTML5 && currentFileUpload != null
569                         && currentFileUpload.getName().equals(fileUpload.getName())) {
570                     // create a new mixed mode (from previous file)
571 
572                     // change multipart body header of previous file into
573                     // multipart list to
574                     // mixedmultipart start, mixedmultipart body header
575 
576                     // change Internal (size()-2 position in multipartHttpDatas)
577                     // from (line starting with *)
578                     // --AaB03x
579                     // * Content-Disposition: form-data; name="files";
580                     // filename="file1.txt"
581                     // Content-Type: text/plain
582                     // to (lines starting with *)
583                     // --AaB03x
584                     // * Content-Disposition: form-data; name="files"
585                     // * Content-Type: multipart/mixed; boundary=BbC04y
586                     // *
587                     // * --BbC04y
588                     // * Content-Disposition: attachment; filename="file1.txt"
589                     // Content-Type: text/plain
590                     initMixedMultipart();
591                     InternalAttribute pastAttribute = (InternalAttribute) multipartHttpDatas.get(multipartHttpDatas
592                             .size() - 2);
593                     // remove past size
594                     globalBodySize -= pastAttribute.size();
595                     StringBuilder replacement = new StringBuilder(
596                             139 + multipartDataBoundary.length() + multipartMixedBoundary.length() * 2 +
597                                     fileUpload.getFilename().length() + fileUpload.getName().length())
598 
599                         .append("--")
600                         .append(multipartDataBoundary)
601                         .append("\r\n")
602 
603                         .append(HttpHeaderNames.CONTENT_DISPOSITION)
604                         .append(": ")
605                         .append(HttpHeaderValues.FORM_DATA)
606                         .append("; ")
607                         .append(HttpHeaderValues.NAME)
608                         .append("=\"")
609                         .append(fileUpload.getName())
610                         .append("\"\r\n")
611 
612                         .append(HttpHeaderNames.CONTENT_TYPE)
613                         .append(": ")
614                         .append(HttpHeaderValues.MULTIPART_MIXED)
615                         .append("; ")
616                         .append(HttpHeaderValues.BOUNDARY)
617                         .append('=')
618                         .append(multipartMixedBoundary)
619                         .append("\r\n\r\n")
620 
621                         .append("--")
622                         .append(multipartMixedBoundary)
623                         .append("\r\n")
624 
625                         .append(HttpHeaderNames.CONTENT_DISPOSITION)
626                         .append(": ")
627                         .append(HttpHeaderValues.ATTACHMENT);
628 
629                     if (!fileUpload.getFilename().isEmpty()) {
630                         replacement.append("; ")
631                                    .append(HttpHeaderValues.FILENAME)
632                                    .append("=\"")
633                                    .append(currentFileUpload.getFilename())
634                                    .append('"');
635                     }
636 
637                     replacement.append("\r\n");
638 
639                     pastAttribute.setValue(replacement.toString(), 1);
640                     pastAttribute.setValue("", 2);
641 
642                     // update past size
643                     globalBodySize += pastAttribute.size();
644 
645                     // now continue
646                     // add mixedmultipart delimiter, mixedmultipart body header
647                     // and
648                     // Data to multipart list
649                     localMixed = true;
650                     duringMixedMode = true;
651                 } else {
652                     // a simple new multipart
653                     // add multipart delimiter, multipart body header and Data
654                     // to multipart list
655                     localMixed = false;
656                     currentFileUpload = fileUpload;
657                     duringMixedMode = false;
658                 }
659             }
660 
661             if (localMixed) {
662                 // add mixedmultipart delimiter, mixedmultipart body header and
663                 // Data to multipart list
664                 internal.addValue("--" + multipartMixedBoundary + "\r\n");
665 
666                 if (fileUpload.getFilename().isEmpty()) {
667                     // Content-Disposition: attachment
668                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": "
669                             + HttpHeaderValues.ATTACHMENT + "\r\n");
670                 } else {
671                     // Content-Disposition: attachment; filename="file1.txt"
672                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": "
673                             + HttpHeaderValues.ATTACHMENT + "; "
674                             + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
675                 }
676             } else {
677                 internal.addValue("--" + multipartDataBoundary + "\r\n");
678 
679                 if (fileUpload.getFilename().isEmpty()) {
680                     // Content-Disposition: form-data; name="files";
681                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
682                             + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"\r\n");
683                 } else {
684                     // Content-Disposition: form-data; name="files";
685                     // filename="file1.txt"
686                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
687                             + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; "
688                             + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
689                 }
690             }
691             // Add Content-Length: xxx
692             internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
693                     fileUpload.length() + "\r\n");
694             // Content-Type: image/gif
695             // Content-Type: text/plain; charset=ISO-8859-1
696             // Content-Transfer-Encoding: binary
697             internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " + fileUpload.getContentType());
698             String contentTransferEncoding = fileUpload.getContentTransferEncoding();
699             if (contentTransferEncoding != null
700                     && contentTransferEncoding.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
701                 internal.addValue("\r\n" + HttpHeaderNames.CONTENT_TRANSFER_ENCODING + ": "
702                         + HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value() + "\r\n\r\n");
703             } else if (fileUpload.getCharset() != null) {
704                 internal.addValue("; " + HttpHeaderValues.CHARSET + '=' + fileUpload.getCharset().name() + "\r\n\r\n");
705             } else {
706                 internal.addValue("\r\n\r\n");
707             }
708             multipartHttpDatas.add(internal);
709             multipartHttpDatas.add(data);
710             globalBodySize += fileUpload.length() + internal.size();
711         }
712     }
713 
714     /**
715      * Iterator to be used when encoding will be called chunk after chunk
716      */
717     private ListIterator<InterfaceHttpData> iterator;
718 
719     /**
720      * Finalize the request by preparing the Header in the request and returns the request ready to be sent.<br>
721      * Once finalized, no data must be added.<br>
722      * If the request does not need chunk (isChunked() == false), this request is the only object to send to the remote
723      * server.
724      *
725      * @return the request object (chunked or not according to size of body)
726      * @throws ErrorDataEncoderException
727      *             if the encoding is in error or if the finalize were already done
728      */
729     public HttpRequest finalizeRequest() throws ErrorDataEncoderException {
730         // Finalize the multipartHttpDatas
731         if (!headerFinalized) {
732             if (isMultipart) {
733                 InternalAttribute internal = new InternalAttribute(charset);
734                 if (duringMixedMode) {
735                     internal.addValue("\r\n--" + multipartMixedBoundary + "--");
736                 }
737                 internal.addValue("\r\n--" + multipartDataBoundary + "--\r\n");
738                 multipartHttpDatas.add(internal);
739                 multipartMixedBoundary = null;
740                 currentFileUpload = null;
741                 duringMixedMode = false;
742                 globalBodySize += internal.size();
743             }
744             headerFinalized = true;
745         } else {
746             throw new ErrorDataEncoderException("Header already encoded");
747         }
748 
749         HttpHeaders headers = request.headers();
750         List<String> contentTypes = headers.getAll(HttpHeaderNames.CONTENT_TYPE);
751         List<String> transferEncoding = headers.getAll(HttpHeaderNames.TRANSFER_ENCODING);
752         if (contentTypes != null) {
753             headers.remove(HttpHeaderNames.CONTENT_TYPE);
754             for (String contentType : contentTypes) {
755                 // "multipart/form-data; boundary=--89421926422648"
756                 String lowercased = contentType.toLowerCase();
757                 if (lowercased.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString()) ||
758                         lowercased.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
759                     // ignore
760                 } else {
761                     headers.add(HttpHeaderNames.CONTENT_TYPE, contentType);
762                 }
763             }
764         }
765         if (isMultipart) {
766             String value = HttpHeaderValues.MULTIPART_FORM_DATA + "; " + HttpHeaderValues.BOUNDARY + '='
767                     + multipartDataBoundary;
768             headers.add(HttpHeaderNames.CONTENT_TYPE, value);
769         } else {
770             // Not multipart
771             headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED);
772         }
773         // Now consider size for chunk or not
774         long realSize = globalBodySize;
775         if (!isMultipart) {
776             realSize -= 1; // last '&' removed
777         }
778         iterator = multipartHttpDatas.listIterator();
779 
780         headers.set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(realSize));
781         if (realSize > HttpPostBodyUtil.chunkSize || isMultipart) {
782             isChunked = true;
783             if (transferEncoding != null) {
784                 headers.remove(HttpHeaderNames.TRANSFER_ENCODING);
785                 for (CharSequence v : transferEncoding) {
786                     if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(v)) {
787                         // ignore
788                     } else {
789                         headers.add(HttpHeaderNames.TRANSFER_ENCODING, v);
790                     }
791                 }
792             }
793             HttpUtil.setTransferEncodingChunked(request, true);
794 
795             // wrap to hide the possible content
796             return new WrappedHttpRequest(request);
797         } else {
798             // get the only one body and set it to the request
799             HttpContent chunk = nextChunk();
800             if (request instanceof FullHttpRequest) {
801                 FullHttpRequest fullRequest = (FullHttpRequest) request;
802                 ByteBuf chunkContent = chunk.content();
803                 if (fullRequest.content() != chunkContent) {
804                     fullRequest.content().clear().writeBytes(chunkContent);
805                     chunkContent.release();
806                 }
807                 return fullRequest;
808             } else {
809                 return new WrappedFullHttpRequest(request, chunk);
810             }
811         }
812     }
813 
814     /**
815      * @return True if the request is by Chunk
816      */
817     public boolean isChunked() {
818         return isChunked;
819     }
820 
821     /**
822      * Encode one attribute
823      *
824      * @return the encoded attribute
825      * @throws ErrorDataEncoderException
826      *             if the encoding is in error
827      */
828     private String encodeAttribute(String s, Charset charset) throws ErrorDataEncoderException {
829         if (s == null) {
830             return "";
831         }
832         try {
833             String encoded = URLEncoder.encode(s, charset.name());
834             if (encoderMode == EncoderMode.RFC3986) {
835                 encoded = encoded.replace(ASTERISK, ASTERISK_REPLACEMENT)
836                                  .replace(PLUS, PLUS_REPLACEMENT)
837                                  .replace(TILDE, TILDE_REPLACEMENT);
838             }
839             return encoded;
840         } catch (UnsupportedEncodingException e) {
841             throw new ErrorDataEncoderException(charset.name(), e);
842         }
843     }
844 
845     /**
846      * The ByteBuf currently used by the encoder
847      */
848     private ByteBuf currentBuffer;
849     /**
850      * The current InterfaceHttpData to encode (used if more chunks are available)
851      */
852     private InterfaceHttpData currentData;
853     /**
854      * If not multipart, does the currentBuffer stands for the Key or for the Value
855      */
856     private boolean isKey = true;
857 
858     /**
859      *
860      * @return the next ByteBuf to send as an HttpChunk and modifying currentBuffer accordingly
861      */
862     private ByteBuf fillByteBuf() {
863         int length = currentBuffer.readableBytes();
864         if (length > HttpPostBodyUtil.chunkSize) {
865             return currentBuffer.readRetainedSlice(HttpPostBodyUtil.chunkSize);
866         } else {
867             // to continue
868             ByteBuf slice = currentBuffer;
869             currentBuffer = null;
870             return slice;
871         }
872     }
873 
874     /**
875      * From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get
876      * sizeleft bytes more into the currentBuffer. This is the Multipart version.
877      *
878      * @param sizeleft
879      *            the number of bytes to try to get from currentData
880      * @return the next HttpChunk or null if not enough bytes were found
881      * @throws ErrorDataEncoderException
882      *             if the encoding is in error
883      */
884     private HttpContent encodeNextChunkMultipart(int sizeleft) throws ErrorDataEncoderException {
885         if (currentData == null) {
886             return null;
887         }
888         ByteBuf buffer;
889         if (currentData instanceof InternalAttribute) {
890             buffer = ((InternalAttribute) currentData).toByteBuf();
891             currentData = null;
892         } else {
893             try {
894                 buffer = ((HttpData) currentData).getChunk(sizeleft);
895             } catch (IOException e) {
896                 throw new ErrorDataEncoderException(e);
897             }
898             if (buffer.capacity() == 0) {
899                 // end for current InterfaceHttpData, need more data
900                 currentData = null;
901                 return null;
902             }
903         }
904         if (currentBuffer == null) {
905             currentBuffer = buffer;
906         } else {
907             currentBuffer = wrappedBuffer(currentBuffer, buffer);
908         }
909         if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
910             currentData = null;
911             return null;
912         }
913         buffer = fillByteBuf();
914         return new DefaultHttpContent(buffer);
915     }
916 
917     /**
918      * From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get
919      * sizeleft bytes more into the currentBuffer. This is the UrlEncoded version.
920      *
921      * @param sizeleft
922      *            the number of bytes to try to get from currentData
923      * @return the next HttpChunk or null if not enough bytes were found
924      * @throws ErrorDataEncoderException
925      *             if the encoding is in error
926      */
927     private HttpContent encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEncoderException {
928         if (currentData == null) {
929             return null;
930         }
931         int size = sizeleft;
932         ByteBuf buffer;
933 
934         // Set name=
935         if (isKey) {
936             String key = currentData.getName();
937             buffer = wrappedBuffer(key.getBytes(charset));
938             isKey = false;
939             if (currentBuffer == null) {
940                 currentBuffer = wrappedBuffer(buffer, wrappedBuffer("=".getBytes(charset)));
941             } else {
942                 currentBuffer = wrappedBuffer(currentBuffer, buffer, wrappedBuffer("=".getBytes(charset)));
943             }
944             // continue
945             size -= buffer.readableBytes() + 1;
946             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
947                 buffer = fillByteBuf();
948                 return new DefaultHttpContent(buffer);
949             }
950         }
951 
952         // Put value into buffer
953         try {
954             buffer = ((HttpData) currentData).getChunk(size);
955         } catch (IOException e) {
956             throw new ErrorDataEncoderException(e);
957         }
958 
959         // Figure out delimiter
960         ByteBuf delimiter = null;
961         if (buffer.readableBytes() < size) {
962             isKey = true;
963             currentData = null;
964             delimiter = iterator.hasNext() ? wrappedBuffer("&".getBytes(charset)) : null;
965         }
966 
967         // End for current InterfaceHttpData, need potentially more data
968         if (buffer.capacity() == 0) {
969             isKey = true;
970             currentData = null;
971             if (currentBuffer == null) {
972                 if (delimiter == null) {
973                     return null;
974                 } else {
975                     currentBuffer = delimiter;
976                 }
977             } else {
978                 if (delimiter != null) {
979                     currentBuffer = wrappedBuffer(currentBuffer, delimiter);
980                 }
981             }
982             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
983                 buffer = fillByteBuf();
984                 return new DefaultHttpContent(buffer);
985             }
986             return null;
987         }
988 
989         // Put it all together: name=value&
990         if (currentBuffer == null) {
991             if (delimiter != null) {
992                 currentBuffer = wrappedBuffer(buffer, delimiter);
993             } else {
994                 currentBuffer = buffer;
995             }
996         } else {
997             if (delimiter != null) {
998                 currentBuffer = wrappedBuffer(currentBuffer, buffer, delimiter);
999             } else {
1000                 currentBuffer = wrappedBuffer(currentBuffer, buffer);
1001             }
1002         }
1003 
1004         if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
1005             return new DefaultHttpContent(fillByteBuf());
1006         }
1007         return null;
1008     }
1009 
1010     @Override
1011     public void close() throws Exception {
1012         // NO since the user can want to reuse (broadcast for instance)
1013         // cleanFiles();
1014     }
1015 
1016     @Deprecated
1017     @Override
1018     public HttpContent readChunk(ChannelHandlerContext ctx) throws Exception {
1019         return readChunk(ctx.alloc());
1020     }
1021 
1022     /**
1023      * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()),
1024      * in order to stop calling this getMethod.
1025      *
1026      * @return the next available HttpChunk
1027      * @throws ErrorDataEncoderException
1028      *             if the encoding is in error
1029      */
1030     @Override
1031     public HttpContent readChunk(ByteBufAllocator allocator) throws Exception {
1032         if (isLastChunkSent) {
1033             return null;
1034         } else {
1035             HttpContent nextChunk = nextChunk();
1036             globalProgress += nextChunk.content().readableBytes();
1037             return nextChunk;
1038         }
1039     }
1040 
1041     /**
1042      * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()),
1043      * in order to stop calling this getMethod.
1044      *
1045      * @return the next available HttpChunk
1046      * @throws ErrorDataEncoderException
1047      *             if the encoding is in error
1048      */
1049     private HttpContent nextChunk() throws ErrorDataEncoderException {
1050         if (isLastChunk) {
1051             isLastChunkSent = true;
1052             return LastHttpContent.EMPTY_LAST_CONTENT;
1053         }
1054         // first test if previous buffer is not empty
1055         int size = calculateRemainingSize();
1056         if (size <= 0) {
1057             // NextChunk from buffer
1058             ByteBuf buffer = fillByteBuf();
1059             return new DefaultHttpContent(buffer);
1060         }
1061         // size > 0
1062         if (currentData != null) {
1063             // continue to read data
1064             HttpContent chunk;
1065             if (isMultipart) {
1066                 chunk = encodeNextChunkMultipart(size);
1067             } else {
1068                 chunk = encodeNextChunkUrlEncoded(size);
1069             }
1070             if (chunk != null) {
1071                 // NextChunk from data
1072                 return chunk;
1073             }
1074             size = calculateRemainingSize();
1075         }
1076         if (!iterator.hasNext()) {
1077             return lastChunk();
1078         }
1079         while (size > 0 && iterator.hasNext()) {
1080             currentData = iterator.next();
1081             HttpContent chunk;
1082             if (isMultipart) {
1083                 chunk = encodeNextChunkMultipart(size);
1084             } else {
1085                 chunk = encodeNextChunkUrlEncoded(size);
1086             }
1087             if (chunk == null) {
1088                 // not enough
1089                 size = calculateRemainingSize();
1090                 continue;
1091             }
1092             // NextChunk from data
1093             return chunk;
1094         }
1095         // end since no more data
1096         return lastChunk();
1097     }
1098 
1099     private int calculateRemainingSize() {
1100         int size = HttpPostBodyUtil.chunkSize;
1101         if (currentBuffer != null) {
1102             size -= currentBuffer.readableBytes();
1103         }
1104         return size;
1105     }
1106 
1107     private HttpContent lastChunk() {
1108         isLastChunk = true;
1109         if (currentBuffer == null) {
1110             isLastChunkSent = true;
1111             // LastChunk with no more data
1112             return LastHttpContent.EMPTY_LAST_CONTENT;
1113         }
1114         // NextChunk as last non empty from buffer
1115         ByteBuf buffer = currentBuffer;
1116         currentBuffer = null;
1117         return new DefaultHttpContent(buffer);
1118     }
1119 
1120     @Override
1121     public boolean isEndOfInput() throws Exception {
1122         return isLastChunkSent;
1123     }
1124 
1125     @Override
1126     public long length() {
1127         return isMultipart? globalBodySize : globalBodySize - 1;
1128     }
1129 
1130     @Override
1131     public long progress() {
1132         return globalProgress;
1133     }
1134 
1135     /**
1136      * Exception when an error occurs while encoding
1137      */
1138     public static class ErrorDataEncoderException extends Exception {
1139         private static final long serialVersionUID = 5020247425493164465L;
1140 
1141         public ErrorDataEncoderException() {
1142         }
1143 
1144         public ErrorDataEncoderException(String msg) {
1145             super(msg);
1146         }
1147 
1148         public ErrorDataEncoderException(Throwable cause) {
1149             super(cause);
1150         }
1151 
1152         public ErrorDataEncoderException(String msg, Throwable cause) {
1153             super(msg, cause);
1154         }
1155     }
1156 
1157     private static class WrappedHttpRequest implements HttpRequest {
1158         private final HttpRequest request;
1159         WrappedHttpRequest(HttpRequest request) {
1160             this.request = request;
1161         }
1162 
1163         @Override
1164         public HttpRequest setProtocolVersion(HttpVersion version) {
1165             request.setProtocolVersion(version);
1166             return this;
1167         }
1168 
1169         @Override
1170         public HttpRequest setMethod(HttpMethod method) {
1171             request.setMethod(method);
1172             return this;
1173         }
1174 
1175         @Override
1176         public HttpRequest setUri(String uri) {
1177             request.setUri(uri);
1178             return this;
1179         }
1180 
1181         @Override
1182         public HttpMethod getMethod() {
1183             return request.method();
1184         }
1185 
1186         @Override
1187         public HttpMethod method() {
1188             return request.method();
1189         }
1190 
1191         @Override
1192         public String getUri() {
1193             return request.uri();
1194         }
1195 
1196         @Override
1197         public String uri() {
1198             return request.uri();
1199         }
1200 
1201         @Override
1202         public HttpVersion getProtocolVersion() {
1203             return request.protocolVersion();
1204         }
1205 
1206         @Override
1207         public HttpVersion protocolVersion() {
1208             return request.protocolVersion();
1209         }
1210 
1211         @Override
1212         public HttpHeaders headers() {
1213             return request.headers();
1214         }
1215 
1216         @Override
1217         public DecoderResult decoderResult() {
1218             return request.decoderResult();
1219         }
1220 
1221         @Override
1222         @Deprecated
1223         public DecoderResult getDecoderResult() {
1224             return request.getDecoderResult();
1225         }
1226 
1227         @Override
1228         public void setDecoderResult(DecoderResult result) {
1229             request.setDecoderResult(result);
1230         }
1231     }
1232 
1233     private static final class WrappedFullHttpRequest extends WrappedHttpRequest implements FullHttpRequest {
1234         private final HttpContent content;
1235 
1236         private WrappedFullHttpRequest(HttpRequest request, HttpContent content) {
1237             super(request);
1238             this.content = content;
1239         }
1240 
1241         @Override
1242         public FullHttpRequest setProtocolVersion(HttpVersion version) {
1243             super.setProtocolVersion(version);
1244             return this;
1245         }
1246 
1247         @Override
1248         public FullHttpRequest setMethod(HttpMethod method) {
1249             super.setMethod(method);
1250             return this;
1251         }
1252 
1253         @Override
1254         public FullHttpRequest setUri(String uri) {
1255             super.setUri(uri);
1256             return this;
1257         }
1258 
1259         @Override
1260         public FullHttpRequest copy() {
1261             return replace(content().copy());
1262         }
1263 
1264         @Override
1265         public FullHttpRequest duplicate() {
1266             return replace(content().duplicate());
1267         }
1268 
1269         @Override
1270         public FullHttpRequest retainedDuplicate() {
1271             return replace(content().retainedDuplicate());
1272         }
1273 
1274         @Override
1275         public FullHttpRequest replace(ByteBuf content) {
1276             DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(protocolVersion(), method(), uri(), content);
1277             duplicate.headers().set(headers());
1278             duplicate.trailingHeaders().set(trailingHeaders());
1279             return duplicate;
1280         }
1281 
1282         @Override
1283         public FullHttpRequest retain(int increment) {
1284             content.retain(increment);
1285             return this;
1286         }
1287 
1288         @Override
1289         public FullHttpRequest retain() {
1290             content.retain();
1291             return this;
1292         }
1293 
1294         @Override
1295         public FullHttpRequest touch() {
1296             content.touch();
1297             return this;
1298         }
1299 
1300         @Override
1301         public FullHttpRequest touch(Object hint) {
1302             content.touch(hint);
1303             return this;
1304         }
1305 
1306         @Override
1307         public ByteBuf content() {
1308             return content.content();
1309         }
1310 
1311         @Override
1312         public HttpHeaders trailingHeaders() {
1313             if (content instanceof LastHttpContent) {
1314                 return ((LastHttpContent) content).trailingHeaders();
1315             } else {
1316                 return EmptyHttpHeaders.INSTANCE;
1317             }
1318         }
1319 
1320         @Override
1321         public int refCnt() {
1322             return content.refCnt();
1323         }
1324 
1325         @Override
1326         public boolean release() {
1327             return content.release();
1328         }
1329 
1330         @Override
1331         public boolean release(int decrement) {
1332             return content.release(decrement);
1333         }
1334     }
1335 }