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             delimiter = iterator.hasNext() ? wrappedBuffer("&".getBytes(charset)) : null;
973         }
974 
975         // End for current InterfaceHttpData, need potentially more data
976         if (buffer.capacity() == 0) {
977             currentData = null;
978             if (currentBuffer == null) {
979                 if (delimiter == null) {
980                     return null;
981                 } else {
982                     currentBuffer = delimiter;
983                 }
984             } else {
985                 if (delimiter != null) {
986                     currentBuffer = wrappedBuffer(currentBuffer, delimiter);
987                 }
988             }
989             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
990                 buffer = fillByteBuf();
991                 return new DefaultHttpContent(buffer);
992             }
993             return null;
994         }
995 
996         // Put it all together: name=value&
997         if (currentBuffer == null) {
998             if (delimiter != null) {
999                 currentBuffer = wrappedBuffer(buffer, delimiter);
1000             } else {
1001                 currentBuffer = buffer;
1002             }
1003         } else {
1004             if (delimiter != null) {
1005                 currentBuffer = wrappedBuffer(currentBuffer, buffer, delimiter);
1006             } else {
1007                 currentBuffer = wrappedBuffer(currentBuffer, buffer);
1008             }
1009         }
1010 
1011         // end for current InterfaceHttpData, need more data
1012         if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
1013             currentData = null;
1014             isKey = true;
1015             return null;
1016         }
1017 
1018         buffer = fillByteBuf();
1019         return new DefaultHttpContent(buffer);
1020     }
1021 
1022     @Override
1023     public void close() throws Exception {
1024         // NO since the user can want to reuse (broadcast for instance)
1025         // cleanFiles();
1026     }
1027 
1028     @Deprecated
1029     @Override
1030     public HttpContent readChunk(ChannelHandlerContext ctx) throws Exception {
1031         return readChunk(ctx.alloc());
1032     }
1033 
1034     /**
1035      * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()),
1036      * in order to stop calling this getMethod.
1037      *
1038      * @return the next available HttpChunk
1039      * @throws ErrorDataEncoderException
1040      *             if the encoding is in error
1041      */
1042     @Override
1043     public HttpContent readChunk(ByteBufAllocator allocator) throws Exception {
1044         if (isLastChunkSent) {
1045             return null;
1046         } else {
1047             HttpContent nextChunk = nextChunk();
1048             globalProgress += nextChunk.content().readableBytes();
1049             return nextChunk;
1050         }
1051     }
1052 
1053     /**
1054      * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()),
1055      * in order to stop calling this getMethod.
1056      *
1057      * @return the next available HttpChunk
1058      * @throws ErrorDataEncoderException
1059      *             if the encoding is in error
1060      */
1061     private HttpContent nextChunk() throws ErrorDataEncoderException {
1062         if (isLastChunk) {
1063             isLastChunkSent = true;
1064             return LastHttpContent.EMPTY_LAST_CONTENT;
1065         }
1066         // first test if previous buffer is not empty
1067         int size = calculateRemainingSize();
1068         if (size <= 0) {
1069             // NextChunk from buffer
1070             ByteBuf buffer = fillByteBuf();
1071             return new DefaultHttpContent(buffer);
1072         }
1073         // size > 0
1074         if (currentData != null) {
1075             // continue to read data
1076             HttpContent chunk;
1077             if (isMultipart) {
1078                 chunk = encodeNextChunkMultipart(size);
1079             } else {
1080                 chunk = encodeNextChunkUrlEncoded(size);
1081             }
1082             if (chunk != null) {
1083                 // NextChunk from data
1084                 return chunk;
1085             }
1086             size = calculateRemainingSize();
1087         }
1088         if (!iterator.hasNext()) {
1089             return lastChunk();
1090         }
1091         while (size > 0 && iterator.hasNext()) {
1092             currentData = iterator.next();
1093             HttpContent chunk;
1094             if (isMultipart) {
1095                 chunk = encodeNextChunkMultipart(size);
1096             } else {
1097                 chunk = encodeNextChunkUrlEncoded(size);
1098             }
1099             if (chunk == null) {
1100                 // not enough
1101                 size = calculateRemainingSize();
1102                 continue;
1103             }
1104             // NextChunk from data
1105             return chunk;
1106         }
1107         // end since no more data
1108         return lastChunk();
1109     }
1110 
1111     private int calculateRemainingSize() {
1112         int size = HttpPostBodyUtil.chunkSize;
1113         if (currentBuffer != null) {
1114             size -= currentBuffer.readableBytes();
1115         }
1116         return size;
1117     }
1118 
1119     private HttpContent lastChunk() {
1120         isLastChunk = true;
1121         if (currentBuffer == null) {
1122             isLastChunkSent = true;
1123             // LastChunk with no more data
1124             return LastHttpContent.EMPTY_LAST_CONTENT;
1125         }
1126         // NextChunk as last non empty from buffer
1127         ByteBuf buffer = currentBuffer;
1128         currentBuffer = null;
1129         return new DefaultHttpContent(buffer);
1130     }
1131 
1132     @Override
1133     public boolean isEndOfInput() throws Exception {
1134         return isLastChunkSent;
1135     }
1136 
1137     @Override
1138     public long length() {
1139         return isMultipart? globalBodySize : globalBodySize - 1;
1140     }
1141 
1142     @Override
1143     public long progress() {
1144         return globalProgress;
1145     }
1146 
1147     /**
1148      * Exception when an error occurs while encoding
1149      */
1150     public static class ErrorDataEncoderException extends Exception {
1151         private static final long serialVersionUID = 5020247425493164465L;
1152 
1153         public ErrorDataEncoderException() {
1154         }
1155 
1156         public ErrorDataEncoderException(String msg) {
1157             super(msg);
1158         }
1159 
1160         public ErrorDataEncoderException(Throwable cause) {
1161             super(cause);
1162         }
1163 
1164         public ErrorDataEncoderException(String msg, Throwable cause) {
1165             super(msg, cause);
1166         }
1167     }
1168 
1169     private static class WrappedHttpRequest implements HttpRequest {
1170         private final HttpRequest request;
1171         WrappedHttpRequest(HttpRequest request) {
1172             this.request = request;
1173         }
1174 
1175         @Override
1176         public HttpRequest setProtocolVersion(HttpVersion version) {
1177             request.setProtocolVersion(version);
1178             return this;
1179         }
1180 
1181         @Override
1182         public HttpRequest setMethod(HttpMethod method) {
1183             request.setMethod(method);
1184             return this;
1185         }
1186 
1187         @Override
1188         public HttpRequest setUri(String uri) {
1189             request.setUri(uri);
1190             return this;
1191         }
1192 
1193         @Override
1194         public HttpMethod getMethod() {
1195             return request.method();
1196         }
1197 
1198         @Override
1199         public HttpMethod method() {
1200             return request.method();
1201         }
1202 
1203         @Override
1204         public String getUri() {
1205             return request.uri();
1206         }
1207 
1208         @Override
1209         public String uri() {
1210             return request.uri();
1211         }
1212 
1213         @Override
1214         public HttpVersion getProtocolVersion() {
1215             return request.protocolVersion();
1216         }
1217 
1218         @Override
1219         public HttpVersion protocolVersion() {
1220             return request.protocolVersion();
1221         }
1222 
1223         @Override
1224         public HttpHeaders headers() {
1225             return request.headers();
1226         }
1227 
1228         @Override
1229         public DecoderResult decoderResult() {
1230             return request.decoderResult();
1231         }
1232 
1233         @Override
1234         @Deprecated
1235         public DecoderResult getDecoderResult() {
1236             return request.getDecoderResult();
1237         }
1238 
1239         @Override
1240         public void setDecoderResult(DecoderResult result) {
1241             request.setDecoderResult(result);
1242         }
1243     }
1244 
1245     private static final class WrappedFullHttpRequest extends WrappedHttpRequest implements FullHttpRequest {
1246         private final HttpContent content;
1247 
1248         private WrappedFullHttpRequest(HttpRequest request, HttpContent content) {
1249             super(request);
1250             this.content = content;
1251         }
1252 
1253         @Override
1254         public FullHttpRequest setProtocolVersion(HttpVersion version) {
1255             super.setProtocolVersion(version);
1256             return this;
1257         }
1258 
1259         @Override
1260         public FullHttpRequest setMethod(HttpMethod method) {
1261             super.setMethod(method);
1262             return this;
1263         }
1264 
1265         @Override
1266         public FullHttpRequest setUri(String uri) {
1267             super.setUri(uri);
1268             return this;
1269         }
1270 
1271         @Override
1272         public FullHttpRequest copy() {
1273             return replace(content().copy());
1274         }
1275 
1276         @Override
1277         public FullHttpRequest duplicate() {
1278             return replace(content().duplicate());
1279         }
1280 
1281         @Override
1282         public FullHttpRequest retainedDuplicate() {
1283             return replace(content().retainedDuplicate());
1284         }
1285 
1286         @Override
1287         public FullHttpRequest replace(ByteBuf content) {
1288             DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(protocolVersion(), method(), uri(), content);
1289             duplicate.headers().set(headers());
1290             duplicate.trailingHeaders().set(trailingHeaders());
1291             return duplicate;
1292         }
1293 
1294         @Override
1295         public FullHttpRequest retain(int increment) {
1296             content.retain(increment);
1297             return this;
1298         }
1299 
1300         @Override
1301         public FullHttpRequest retain() {
1302             content.retain();
1303             return this;
1304         }
1305 
1306         @Override
1307         public FullHttpRequest touch() {
1308             content.touch();
1309             return this;
1310         }
1311 
1312         @Override
1313         public FullHttpRequest touch(Object hint) {
1314             content.touch(hint);
1315             return this;
1316         }
1317 
1318         @Override
1319         public ByteBuf content() {
1320             return content.content();
1321         }
1322 
1323         @Override
1324         public HttpHeaders trailingHeaders() {
1325             if (content instanceof LastHttpContent) {
1326                 return ((LastHttpContent) content).trailingHeaders();
1327             } else {
1328                 return EmptyHttpHeaders.INSTANCE;
1329             }
1330         }
1331 
1332         @Override
1333         public int refCnt() {
1334             return content.refCnt();
1335         }
1336 
1337         @Override
1338         public boolean release() {
1339             return content.release();
1340         }
1341 
1342         @Override
1343         public boolean release(int decrement) {
1344             return content.release(decrement);
1345         }
1346     }
1347 }