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