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    *   http://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.PlatformDependent;
38  import io.netty.util.internal.StringUtil;
39  
40  import java.io.File;
41  import java.io.IOException;
42  import java.io.UnsupportedEncodingException;
43  import java.net.URLEncoder;
44  import java.nio.charset.Charset;
45  import java.util.ArrayList;
46  import java.util.List;
47  import java.util.ListIterator;
48  import java.util.Map;
49  import java.util.regex.Pattern;
50  
51  import static io.netty.buffer.Unpooled.wrappedBuffer;
52  import static io.netty.util.internal.ObjectUtil.checkNotNull;
53  import static java.util.AbstractMap.SimpleImmutableEntry;
54  
55  /**
56   * This encoder will help to encode Request for a FORM as POST.
57   *
58   * <P>According to RFC 7231, POST, PUT and OPTIONS allow to have a body.
59   * This encoder will support widely all methods except TRACE since the RFC notes
60   * for GET, DELETE, HEAD and CONNECT: (replaces XXX by one of these methods)</P>
61   * <P>"A payload within a XXX request message has no defined semantics;
62   * sending a payload body on a XXX request might cause some existing
63   * implementations to reject the request."</P>
64   * <P>On the contrary, for TRACE method, RFC says:</P>
65   * <P>"A client MUST NOT send a message body in a TRACE request."</P>
66   */
67  public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
68  
69      /**
70       * Different modes to use to encode form data.
71       */
72      public enum EncoderMode {
73          /**
74           * Legacy mode which should work for most. It is known to not work with OAUTH. For OAUTH use
75           * {@link EncoderMode#RFC3986}. The W3C form recommendations this for submitting post form data.
76           */
77          RFC1738,
78  
79          /**
80           * Mode which is more new and is used for OAUTH
81           */
82          RFC3986,
83  
84          /**
85           * The HTML5 spec disallows mixed mode in multipart/form-data
86           * requests. More concretely this means that more files submitted
87           * under the same name will not be encoded using mixed mode, but
88           * will be treated as distinct fields.
89           *
90           * Reference:
91           *   http://www.w3.org/TR/html5/forms.html#multipart-form-data
92           */
93          HTML5
94      }
95  
96      @SuppressWarnings("rawtypes")
97      private static final Map.Entry[] percentEncodings;
98  
99      static {
100         percentEncodings = new Map.Entry[] {
101                 new SimpleImmutableEntry<Pattern, String>(Pattern.compile("\\*"), "%2A"),
102                 new SimpleImmutableEntry<Pattern, String>(Pattern.compile("\\+"), "%20"),
103                 new SimpleImmutableEntry<Pattern, String>(Pattern.compile("~"), "%7E")
104         };
105     }
106 
107     /**
108      * Factory used to create InterfaceHttpData
109      */
110     private final HttpDataFactory factory;
111 
112     /**
113      * Request to encode
114      */
115     private final HttpRequest request;
116 
117     /**
118      * Default charset to use
119      */
120     private final Charset charset;
121 
122     /**
123      * Chunked false by default
124      */
125     private boolean isChunked;
126 
127     /**
128      * InterfaceHttpData for Body (without encoding)
129      */
130     private final List<InterfaceHttpData> bodyListDatas;
131     /**
132      * The final Multipart List of InterfaceHttpData including encoding
133      */
134     final List<InterfaceHttpData> multipartHttpDatas;
135 
136     /**
137      * Does this request is a Multipart request
138      */
139     private final boolean isMultipart;
140 
141     /**
142      * If multipart, this is the boundary for the flobal multipart
143      */
144     String multipartDataBoundary;
145 
146     /**
147      * If multipart, there could be internal multiparts (mixed) to the global multipart. Only one level is allowed.
148      */
149     String multipartMixedBoundary;
150     /**
151      * To check if the header has been finalized
152      */
153     private boolean headerFinalized;
154 
155     private final EncoderMode encoderMode;
156 
157     /**
158      *
159      * @param request
160      *            the request to encode
161      * @param multipart
162      *            True if the FORM is a ENCTYPE="multipart/form-data"
163      * @throws NullPointerException
164      *             for request
165      * @throws ErrorDataEncoderException
166      *             if the request is a TRACE
167      */
168     public HttpPostRequestEncoder(HttpRequest request, boolean multipart) throws ErrorDataEncoderException {
169         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, multipart,
170                 HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738);
171     }
172 
173     /**
174      *
175      * @param factory
176      *            the factory used to create InterfaceHttpData
177      * @param request
178      *            the request to encode
179      * @param multipart
180      *            True if the FORM is a ENCTYPE="multipart/form-data"
181      * @throws NullPointerException
182      *             for request and factory
183      * @throws ErrorDataEncoderException
184      *             if the request is a TRACE
185      */
186     public HttpPostRequestEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart)
187             throws ErrorDataEncoderException {
188         this(factory, request, multipart, HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738);
189     }
190 
191     /**
192      *
193      * @param factory
194      *            the factory used to create InterfaceHttpData
195      * @param request
196      *            the request to encode
197      * @param multipart
198      *            True if the FORM is a ENCTYPE="multipart/form-data"
199      * @param charset
200      *            the charset to use as default
201      * @param encoderMode
202      *            the mode for the encoder to use. See {@link EncoderMode} for the details.
203      * @throws NullPointerException
204      *             for request or charset or factory
205      * @throws ErrorDataEncoderException
206      *             if the request is a TRACE
207      */
208     public HttpPostRequestEncoder(
209             HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset,
210             EncoderMode encoderMode)
211             throws ErrorDataEncoderException {
212         this.request = checkNotNull(request, "request");
213         this.charset = checkNotNull(charset, "charset");
214         this.factory = checkNotNull(factory, "factory");
215         if (HttpMethod.TRACE.equals(request.method())) {
216             throw new ErrorDataEncoderException("Cannot create a Encoder if request is a TRACE");
217         }
218         // Fill default values
219         bodyListDatas = new ArrayList<InterfaceHttpData>();
220         // default mode
221         isLastChunk = false;
222         isLastChunkSent = false;
223         isMultipart = multipart;
224         multipartHttpDatas = new ArrayList<InterfaceHttpData>();
225         this.encoderMode = encoderMode;
226         if (isMultipart) {
227             initDataMultipart();
228         }
229     }
230 
231     /**
232      * Clean all HttpDatas (on Disk) for the current request.
233      */
234     public void cleanFiles() {
235         factory.cleanRequestHttpData(request);
236     }
237 
238     /**
239      * Does the last non empty chunk already encoded so that next chunk will be empty (last chunk)
240      */
241     private boolean isLastChunk;
242     /**
243      * Last chunk already sent
244      */
245     private boolean isLastChunkSent;
246     /**
247      * The current FileUpload that is currently in encode process
248      */
249     private FileUpload currentFileUpload;
250     /**
251      * While adding a FileUpload, is the multipart currently in Mixed Mode
252      */
253     private boolean duringMixedMode;
254     /**
255      * Global Body size
256      */
257     private long globalBodySize;
258     /**
259      * Global Transfer progress
260      */
261     private long globalProgress;
262 
263     /**
264      * True if this request is a Multipart request
265      *
266      * @return True if this request is a Multipart request
267      */
268     public boolean isMultipart() {
269         return isMultipart;
270     }
271 
272     /**
273      * Init the delimiter for Global Part (Data).
274      */
275     private void initDataMultipart() {
276         multipartDataBoundary = getNewMultipartDelimiter();
277     }
278 
279     /**
280      * Init the delimiter for Mixed Part (Mixed).
281      */
282     private void initMixedMultipart() {
283         multipartMixedBoundary = getNewMultipartDelimiter();
284     }
285 
286     /**
287      *
288      * @return a newly generated Delimiter (either for DATA or MIXED)
289      */
290     private static String getNewMultipartDelimiter() {
291         // construct a generated delimiter
292         return Long.toHexString(PlatformDependent.threadLocalRandom().nextLong());
293     }
294 
295     /**
296      * This getMethod returns a List of all InterfaceHttpData from body part.<br>
297 
298      * @return the list of InterfaceHttpData from Body part
299      */
300     public List<InterfaceHttpData> getBodyListAttributes() {
301         return bodyListDatas;
302     }
303 
304     /**
305      * Set the Body HttpDatas list
306      *
307      * @throws NullPointerException
308      *             for datas
309      * @throws ErrorDataEncoderException
310      *             if the encoding is in error or if the finalize were already done
311      */
312     public void setBodyHttpDatas(List<InterfaceHttpData> datas) throws ErrorDataEncoderException {
313         if (datas == null) {
314             throw new NullPointerException("datas");
315         }
316         globalBodySize = 0;
317         bodyListDatas.clear();
318         currentFileUpload = null;
319         duringMixedMode = false;
320         multipartHttpDatas.clear();
321         for (InterfaceHttpData data : datas) {
322             addBodyHttpData(data);
323         }
324     }
325 
326     /**
327      * Add a simple attribute in the body as Name=Value
328      *
329      * @param name
330      *            name of the parameter
331      * @param value
332      *            the value of the parameter
333      * @throws NullPointerException
334      *             for name
335      * @throws ErrorDataEncoderException
336      *             if the encoding is in error or if the finalize were already done
337      */
338     public void addBodyAttribute(String name, String value) throws ErrorDataEncoderException {
339         String svalue = value != null? value : StringUtil.EMPTY_STRING;
340         Attribute data = factory.createAttribute(request, checkNotNull(name, "name"), svalue);
341         addBodyHttpData(data);
342     }
343 
344     /**
345      * Add a file as a FileUpload
346      *
347      * @param name
348      *            the name of the parameter
349      * @param file
350      *            the file to be uploaded (if not Multipart mode, only the filename will be included)
351      * @param contentType
352      *            the associated contentType for the File
353      * @param isText
354      *            True if this file should be transmitted in Text format (else binary)
355      * @throws NullPointerException
356      *             for name and file
357      * @throws ErrorDataEncoderException
358      *             if the encoding is in error or if the finalize were already done
359      */
360     public void addBodyFileUpload(String name, File file, String contentType, boolean isText)
361             throws ErrorDataEncoderException {
362         addBodyFileUpload(name, file.getName(), file, contentType, isText);
363     }
364 
365     /**
366      * Add a file as a FileUpload
367      *
368      * @param name
369      *            the name of the parameter
370      * @param file
371      *            the file to be uploaded (if not Multipart mode, only the filename will be included)
372      * @param filename
373      *            the filename to use for this File part, empty String will be ignored by
374      *            the encoder
375      * @param contentType
376      *            the associated contentType for the File
377      * @param isText
378      *            True if this file should be transmitted in Text format (else binary)
379      * @throws NullPointerException
380      *             for name and file
381      * @throws ErrorDataEncoderException
382      *             if the encoding is in error or if the finalize were already done
383      */
384     public void addBodyFileUpload(String name, String filename, File file, String contentType, boolean isText)
385             throws ErrorDataEncoderException {
386         checkNotNull(name, "name");
387         checkNotNull(file, "file");
388         if (filename == null) {
389             filename = StringUtil.EMPTY_STRING;
390         }
391         String scontentType = contentType;
392         String contentTransferEncoding = null;
393         if (contentType == null) {
394             if (isText) {
395                 scontentType = HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE;
396             } else {
397                 scontentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
398             }
399         }
400         if (!isText) {
401             contentTransferEncoding = HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value();
402         }
403         FileUpload fileUpload = factory.createFileUpload(request, name, filename, scontentType,
404                 contentTransferEncoding, null, file.length());
405         try {
406             fileUpload.setContent(file);
407         } catch (IOException e) {
408             throw new ErrorDataEncoderException(e);
409         }
410         addBodyHttpData(fileUpload);
411     }
412 
413     /**
414      * Add a series of Files associated with one File parameter
415      *
416      * @param name
417      *            the name of the parameter
418      * @param file
419      *            the array of files
420      * @param contentType
421      *            the array of content Types associated with each file
422      * @param isText
423      *            the array of isText attribute (False meaning binary mode) for each file
424      * @throws IllegalArgumentException
425      *             also throws if array have different sizes
426      * @throws ErrorDataEncoderException
427      *             if the encoding is in error or if the finalize were already done
428      */
429     public void addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText)
430             throws ErrorDataEncoderException {
431         if (file.length != contentType.length && file.length != isText.length) {
432             throw new IllegalArgumentException("Different array length");
433         }
434         for (int i = 0; i < file.length; i++) {
435             addBodyFileUpload(name, file[i], contentType[i], isText[i]);
436         }
437     }
438 
439     /**
440      * Add the InterfaceHttpData to the Body list
441      *
442      * @throws NullPointerException
443      *             for data
444      * @throws ErrorDataEncoderException
445      *             if the encoding is in error or if the finalize were already done
446      */
447     public void addBodyHttpData(InterfaceHttpData data) throws ErrorDataEncoderException {
448         if (headerFinalized) {
449             throw new ErrorDataEncoderException("Cannot add value once finalized");
450         }
451         bodyListDatas.add(checkNotNull(data, "data"));
452         if (!isMultipart) {
453             if (data instanceof Attribute) {
454                 Attribute attribute = (Attribute) data;
455                 try {
456                     // name=value& with encoded name and attribute
457                     String key = encodeAttribute(attribute.getName(), charset);
458                     String value = encodeAttribute(attribute.getValue(), charset);
459                     Attribute newattribute = factory.createAttribute(request, key, value);
460                     multipartHttpDatas.add(newattribute);
461                     globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
462                 } catch (IOException e) {
463                     throw new ErrorDataEncoderException(e);
464                 }
465             } else if (data instanceof FileUpload) {
466                 // since not Multipart, only name=filename => Attribute
467                 FileUpload fileUpload = (FileUpload) data;
468                 // name=filename& with encoded name and filename
469                 String key = encodeAttribute(fileUpload.getName(), charset);
470                 String value = encodeAttribute(fileUpload.getFilename(), charset);
471                 Attribute newattribute = factory.createAttribute(request, key, value);
472                 multipartHttpDatas.add(newattribute);
473                 globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
474             }
475             return;
476         }
477         /*
478          * Logic:
479          * if not Attribute:
480          *      add Data to body list
481          *      if (duringMixedMode)
482          *          add endmixedmultipart delimiter
483          *          currentFileUpload = null
484          *          duringMixedMode = false;
485          *      add multipart delimiter, multipart body header and Data to multipart list
486          *      reset currentFileUpload, duringMixedMode
487          * if FileUpload: take care of multiple file for one field => mixed mode
488          *      if (duringMixedMode)
489          *          if (currentFileUpload.name == data.name)
490          *              add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
491          *          else
492          *              add endmixedmultipart delimiter, multipart body header and Data to multipart list
493          *              currentFileUpload = data
494          *              duringMixedMode = false;
495          *      else
496          *          if (currentFileUpload.name == data.name)
497          *              change multipart body header of previous file into multipart list to
498          *                      mixedmultipart start, mixedmultipart body header
499          *              add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
500          *              duringMixedMode = true
501          *          else
502          *              add multipart delimiter, multipart body header and Data to multipart list
503          *              currentFileUpload = data
504          *              duringMixedMode = false;
505          * Do not add last delimiter! Could be:
506          * if duringmixedmode: endmixedmultipart + endmultipart
507          * else only endmultipart
508          */
509         if (data instanceof Attribute) {
510             if (duringMixedMode) {
511                 InternalAttribute internal = new InternalAttribute(charset);
512                 internal.addValue("\r\n--" + multipartMixedBoundary + "--");
513                 multipartHttpDatas.add(internal);
514                 multipartMixedBoundary = null;
515                 currentFileUpload = null;
516                 duringMixedMode = false;
517             }
518             InternalAttribute internal = new InternalAttribute(charset);
519             if (!multipartHttpDatas.isEmpty()) {
520                 // previously a data field so CRLF
521                 internal.addValue("\r\n");
522             }
523             internal.addValue("--" + multipartDataBoundary + "\r\n");
524             // content-disposition: form-data; name="field1"
525             Attribute attribute = (Attribute) data;
526             internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
527                     + HttpHeaderValues.NAME + "=\"" + attribute.getName() + "\"\r\n");
528             // Add Content-Length: xxx
529             internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
530                     attribute.length() + "\r\n");
531             Charset localcharset = attribute.getCharset();
532             if (localcharset != null) {
533                 // Content-Type: text/plain; charset=charset
534                 internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " +
535                         HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE + "; " +
536                         HttpHeaderValues.CHARSET + '='
537                         + localcharset.name() + "\r\n");
538             }
539             // CRLF between body header and data
540             internal.addValue("\r\n");
541             multipartHttpDatas.add(internal);
542             multipartHttpDatas.add(data);
543             globalBodySize += attribute.length() + internal.size();
544         } else if (data instanceof FileUpload) {
545             FileUpload fileUpload = (FileUpload) data;
546             InternalAttribute internal = new InternalAttribute(charset);
547             if (!multipartHttpDatas.isEmpty()) {
548                 // previously a data field so CRLF
549                 internal.addValue("\r\n");
550             }
551             boolean localMixed;
552             if (duringMixedMode) {
553                 if (currentFileUpload != null && currentFileUpload.getName().equals(fileUpload.getName())) {
554                     // continue a mixed mode
555 
556                     localMixed = true;
557                 } else {
558                     // end a mixed mode
559 
560                     // add endmixedmultipart delimiter, multipart body header
561                     // and
562                     // Data to multipart list
563                     internal.addValue("--" + multipartMixedBoundary + "--");
564                     multipartHttpDatas.add(internal);
565                     multipartMixedBoundary = null;
566                     // start a new one (could be replaced if mixed start again
567                     // from here
568                     internal = new InternalAttribute(charset);
569                     internal.addValue("\r\n");
570                     localMixed = false;
571                     // new currentFileUpload and no more in Mixed mode
572                     currentFileUpload = fileUpload;
573                     duringMixedMode = false;
574                 }
575             } else {
576                 if (encoderMode != EncoderMode.HTML5 && currentFileUpload != null
577                         && currentFileUpload.getName().equals(fileUpload.getName())) {
578                     // create a new mixed mode (from previous file)
579 
580                     // change multipart body header of previous file into
581                     // multipart list to
582                     // mixedmultipart start, mixedmultipart body header
583 
584                     // change Internal (size()-2 position in multipartHttpDatas)
585                     // from (line starting with *)
586                     // --AaB03x
587                     // * Content-Disposition: form-data; name="files";
588                     // filename="file1.txt"
589                     // Content-Type: text/plain
590                     // to (lines starting with *)
591                     // --AaB03x
592                     // * Content-Disposition: form-data; name="files"
593                     // * Content-Type: multipart/mixed; boundary=BbC04y
594                     // *
595                     // * --BbC04y
596                     // * Content-Disposition: attachment; filename="file1.txt"
597                     // Content-Type: text/plain
598                     initMixedMultipart();
599                     InternalAttribute pastAttribute = (InternalAttribute) multipartHttpDatas.get(multipartHttpDatas
600                             .size() - 2);
601                     // remove past size
602                     globalBodySize -= pastAttribute.size();
603                     StringBuilder replacement = new StringBuilder(
604                             139 + multipartDataBoundary.length() + multipartMixedBoundary.length() * 2 +
605                                     fileUpload.getFilename().length() + fileUpload.getName().length())
606 
607                         .append("--")
608                         .append(multipartDataBoundary)
609                         .append("\r\n")
610 
611                         .append(HttpHeaderNames.CONTENT_DISPOSITION)
612                         .append(": ")
613                         .append(HttpHeaderValues.FORM_DATA)
614                         .append("; ")
615                         .append(HttpHeaderValues.NAME)
616                         .append("=\"")
617                         .append(fileUpload.getName())
618                         .append("\"\r\n")
619 
620                         .append(HttpHeaderNames.CONTENT_TYPE)
621                         .append(": ")
622                         .append(HttpHeaderValues.MULTIPART_MIXED)
623                         .append("; ")
624                         .append(HttpHeaderValues.BOUNDARY)
625                         .append('=')
626                         .append(multipartMixedBoundary)
627                         .append("\r\n\r\n")
628 
629                         .append("--")
630                         .append(multipartMixedBoundary)
631                         .append("\r\n")
632 
633                         .append(HttpHeaderNames.CONTENT_DISPOSITION)
634                         .append(": ")
635                         .append(HttpHeaderValues.ATTACHMENT);
636 
637                     if (!fileUpload.getFilename().isEmpty()) {
638                         replacement.append("; ")
639                                    .append(HttpHeaderValues.FILENAME)
640                                    .append("=\"")
641                                    .append(fileUpload.getFilename())
642                                    .append('"');
643                     }
644 
645                     replacement.append("\r\n");
646 
647                     pastAttribute.setValue(replacement.toString(), 1);
648                     pastAttribute.setValue("", 2);
649 
650                     // update past size
651                     globalBodySize += pastAttribute.size();
652 
653                     // now continue
654                     // add mixedmultipart delimiter, mixedmultipart body header
655                     // and
656                     // Data to multipart list
657                     localMixed = true;
658                     duringMixedMode = true;
659                 } else {
660                     // a simple new multipart
661                     // add multipart delimiter, multipart body header and Data
662                     // to multipart list
663                     localMixed = false;
664                     currentFileUpload = fileUpload;
665                     duringMixedMode = false;
666                 }
667             }
668 
669             if (localMixed) {
670                 // add mixedmultipart delimiter, mixedmultipart body header and
671                 // Data to multipart list
672                 internal.addValue("--" + multipartMixedBoundary + "\r\n");
673 
674                 if (fileUpload.getFilename().isEmpty()) {
675                     // Content-Disposition: attachment
676                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": "
677                             + HttpHeaderValues.ATTACHMENT + "\r\n");
678                 } else {
679                     // Content-Disposition: attachment; filename="file1.txt"
680                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": "
681                             + HttpHeaderValues.ATTACHMENT + "; "
682                             + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
683                 }
684             } else {
685                 internal.addValue("--" + multipartDataBoundary + "\r\n");
686 
687                 if (fileUpload.getFilename().isEmpty()) {
688                     // Content-Disposition: form-data; name="files";
689                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
690                             + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"\r\n");
691                 } else {
692                     // Content-Disposition: form-data; name="files";
693                     // filename="file1.txt"
694                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
695                             + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; "
696                             + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
697                 }
698             }
699             // Add Content-Length: xxx
700             internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
701                     fileUpload.length() + "\r\n");
702             // Content-Type: image/gif
703             // Content-Type: text/plain; charset=ISO-8859-1
704             // Content-Transfer-Encoding: binary
705             internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " + fileUpload.getContentType());
706             String contentTransferEncoding = fileUpload.getContentTransferEncoding();
707             if (contentTransferEncoding != null
708                     && contentTransferEncoding.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
709                 internal.addValue("\r\n" + HttpHeaderNames.CONTENT_TRANSFER_ENCODING + ": "
710                         + HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value() + "\r\n\r\n");
711             } else if (fileUpload.getCharset() != null) {
712                 internal.addValue("; " + HttpHeaderValues.CHARSET + '=' + fileUpload.getCharset().name() + "\r\n\r\n");
713             } else {
714                 internal.addValue("\r\n\r\n");
715             }
716             multipartHttpDatas.add(internal);
717             multipartHttpDatas.add(data);
718             globalBodySize += fileUpload.length() + internal.size();
719         }
720     }
721 
722     /**
723      * Iterator to be used when encoding will be called chunk after chunk
724      */
725     private ListIterator<InterfaceHttpData> iterator;
726 
727     /**
728      * Finalize the request by preparing the Header in the request and returns the request ready to be sent.<br>
729      * Once finalized, no data must be added.<br>
730      * If the request does not need chunk (isChunked() == false), this request is the only object to send to the remote
731      * server.
732      *
733      * @return the request object (chunked or not according to size of body)
734      * @throws ErrorDataEncoderException
735      *             if the encoding is in error or if the finalize were already done
736      */
737     public HttpRequest finalizeRequest() throws ErrorDataEncoderException {
738         // Finalize the multipartHttpDatas
739         if (!headerFinalized) {
740             if (isMultipart) {
741                 InternalAttribute internal = new InternalAttribute(charset);
742                 if (duringMixedMode) {
743                     internal.addValue("\r\n--" + multipartMixedBoundary + "--");
744                 }
745                 internal.addValue("\r\n--" + multipartDataBoundary + "--\r\n");
746                 multipartHttpDatas.add(internal);
747                 multipartMixedBoundary = null;
748                 currentFileUpload = null;
749                 duringMixedMode = false;
750                 globalBodySize += internal.size();
751             }
752             headerFinalized = true;
753         } else {
754             throw new ErrorDataEncoderException("Header already encoded");
755         }
756 
757         HttpHeaders headers = request.headers();
758         List<String> contentTypes = headers.getAll(HttpHeaderNames.CONTENT_TYPE);
759         List<String> transferEncoding = headers.getAll(HttpHeaderNames.TRANSFER_ENCODING);
760         if (contentTypes != null) {
761             headers.remove(HttpHeaderNames.CONTENT_TYPE);
762             for (String contentType : contentTypes) {
763                 // "multipart/form-data; boundary=--89421926422648"
764                 String lowercased = contentType.toLowerCase();
765                 if (lowercased.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString()) ||
766                         lowercased.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
767                     // ignore
768                 } else {
769                     headers.add(HttpHeaderNames.CONTENT_TYPE, contentType);
770                 }
771             }
772         }
773         if (isMultipart) {
774             String value = HttpHeaderValues.MULTIPART_FORM_DATA + "; " + HttpHeaderValues.BOUNDARY + '='
775                     + multipartDataBoundary;
776             headers.add(HttpHeaderNames.CONTENT_TYPE, value);
777         } else {
778             // Not multipart
779             headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED);
780         }
781         // Now consider size for chunk or not
782         long realSize = globalBodySize;
783         if (!isMultipart) {
784             realSize -= 1; // last '&' removed
785         }
786         iterator = multipartHttpDatas.listIterator();
787 
788         headers.set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(realSize));
789         if (realSize > HttpPostBodyUtil.chunkSize || isMultipart) {
790             isChunked = true;
791             if (transferEncoding != null) {
792                 headers.remove(HttpHeaderNames.TRANSFER_ENCODING);
793                 for (CharSequence v : transferEncoding) {
794                     if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(v)) {
795                         // ignore
796                     } else {
797                         headers.add(HttpHeaderNames.TRANSFER_ENCODING, v);
798                     }
799                 }
800             }
801             HttpUtil.setTransferEncodingChunked(request, true);
802 
803             // wrap to hide the possible content
804             return new WrappedHttpRequest(request);
805         } else {
806             // get the only one body and set it to the request
807             HttpContent chunk = nextChunk();
808             if (request instanceof FullHttpRequest) {
809                 FullHttpRequest fullRequest = (FullHttpRequest) request;
810                 ByteBuf chunkContent = chunk.content();
811                 if (fullRequest.content() != chunkContent) {
812                     fullRequest.content().clear().writeBytes(chunkContent);
813                     chunkContent.release();
814                 }
815                 return fullRequest;
816             } else {
817                 return new WrappedFullHttpRequest(request, chunk);
818             }
819         }
820     }
821 
822     /**
823      * @return True if the request is by Chunk
824      */
825     public boolean isChunked() {
826         return isChunked;
827     }
828 
829     /**
830      * Encode one attribute
831      *
832      * @return the encoded attribute
833      * @throws ErrorDataEncoderException
834      *             if the encoding is in error
835      */
836     @SuppressWarnings("unchecked")
837     private String encodeAttribute(String s, Charset charset) throws ErrorDataEncoderException {
838         if (s == null) {
839             return "";
840         }
841         try {
842             String encoded = URLEncoder.encode(s, charset.name());
843             if (encoderMode == EncoderMode.RFC3986) {
844                 for (Map.Entry<Pattern, String> entry : percentEncodings) {
845                     String replacement = entry.getValue();
846                     encoded = entry.getKey().matcher(encoded).replaceAll(replacement);
847                 }
848             }
849             return encoded;
850         } catch (UnsupportedEncodingException e) {
851             throw new ErrorDataEncoderException(charset.name(), e);
852         }
853     }
854 
855     /**
856      * The ByteBuf currently used by the encoder
857      */
858     private ByteBuf currentBuffer;
859     /**
860      * The current InterfaceHttpData to encode (used if more chunks are available)
861      */
862     private InterfaceHttpData currentData;
863     /**
864      * If not multipart, does the currentBuffer stands for the Key or for the Value
865      */
866     private boolean isKey = true;
867 
868     /**
869      *
870      * @return the next ByteBuf to send as a HttpChunk and modifying currentBuffer accordingly
871      */
872     private ByteBuf fillByteBuf() {
873         int length = currentBuffer.readableBytes();
874         if (length > HttpPostBodyUtil.chunkSize) {
875             return currentBuffer.readRetainedSlice(HttpPostBodyUtil.chunkSize);
876         } else {
877             // to continue
878             ByteBuf slice = currentBuffer;
879             currentBuffer = null;
880             return slice;
881         }
882     }
883 
884     /**
885      * From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get
886      * sizeleft bytes more into the currentBuffer. This is the Multipart version.
887      *
888      * @param sizeleft
889      *            the number of bytes to try to get from currentData
890      * @return the next HttpChunk or null if not enough bytes were found
891      * @throws ErrorDataEncoderException
892      *             if the encoding is in error
893      */
894     private HttpContent encodeNextChunkMultipart(int sizeleft) throws ErrorDataEncoderException {
895         if (currentData == null) {
896             return null;
897         }
898         ByteBuf buffer;
899         if (currentData instanceof InternalAttribute) {
900             buffer = ((InternalAttribute) currentData).toByteBuf();
901             currentData = null;
902         } else {
903             try {
904                 buffer = ((HttpData) currentData).getChunk(sizeleft);
905             } catch (IOException e) {
906                 throw new ErrorDataEncoderException(e);
907             }
908             if (buffer.capacity() == 0) {
909                 // end for current InterfaceHttpData, need more data
910                 currentData = null;
911                 return null;
912             }
913         }
914         if (currentBuffer == null) {
915             currentBuffer = buffer;
916         } else {
917             currentBuffer = wrappedBuffer(currentBuffer, buffer);
918         }
919         if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
920             currentData = null;
921             return null;
922         }
923         buffer = fillByteBuf();
924         return new DefaultHttpContent(buffer);
925     }
926 
927     /**
928      * From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get
929      * sizeleft bytes more into the currentBuffer. This is the UrlEncoded version.
930      *
931      * @param sizeleft
932      *            the number of bytes to try to get from currentData
933      * @return the next HttpChunk or null if not enough bytes were found
934      * @throws ErrorDataEncoderException
935      *             if the encoding is in error
936      */
937     private HttpContent encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEncoderException {
938         if (currentData == null) {
939             return null;
940         }
941         int size = sizeleft;
942         ByteBuf buffer;
943 
944         // Set name=
945         if (isKey) {
946             String key = currentData.getName();
947             buffer = wrappedBuffer(key.getBytes());
948             isKey = false;
949             if (currentBuffer == null) {
950                 currentBuffer = wrappedBuffer(buffer, wrappedBuffer("=".getBytes()));
951             } else {
952                 currentBuffer = wrappedBuffer(currentBuffer, buffer, wrappedBuffer("=".getBytes()));
953             }
954             // continue
955             size -= buffer.readableBytes() + 1;
956             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
957                 buffer = fillByteBuf();
958                 return new DefaultHttpContent(buffer);
959             }
960         }
961 
962         // Put value into buffer
963         try {
964             buffer = ((HttpData) currentData).getChunk(size);
965         } catch (IOException e) {
966             throw new ErrorDataEncoderException(e);
967         }
968 
969         // Figure out delimiter
970         ByteBuf delimiter = null;
971         if (buffer.readableBytes() < size) {
972             isKey = true;
973             delimiter = iterator.hasNext() ? wrappedBuffer("&".getBytes()) : null;
974         }
975 
976         // End for current InterfaceHttpData, need potentially more data
977         if (buffer.capacity() == 0) {
978             currentData = null;
979             if (currentBuffer == null) {
980                 currentBuffer = delimiter;
981             } else {
982                 if (delimiter != null) {
983                     currentBuffer = wrappedBuffer(currentBuffer, delimiter);
984                 }
985             }
986             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
987                 buffer = fillByteBuf();
988                 return new DefaultHttpContent(buffer);
989             }
990             return null;
991         }
992 
993         // Put it all together: name=value&
994         if (currentBuffer == null) {
995             if (delimiter != null) {
996                 currentBuffer = wrappedBuffer(buffer, delimiter);
997             } else {
998                 currentBuffer = buffer;
999             }
1000         } else {
1001             if (delimiter != null) {
1002                 currentBuffer = wrappedBuffer(currentBuffer, buffer, delimiter);
1003             } else {
1004                 currentBuffer = wrappedBuffer(currentBuffer, buffer);
1005             }
1006         }
1007 
1008         // end for current InterfaceHttpData, need more data
1009         if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
1010             currentData = null;
1011             isKey = true;
1012             return null;
1013         }
1014 
1015         buffer = fillByteBuf();
1016         return new DefaultHttpContent(buffer);
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 }