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