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