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.handler.codec.DecoderException;
19  import io.netty.handler.codec.http.HttpConstants;
20  import io.netty.handler.codec.http.HttpContent;
21  import io.netty.handler.codec.http.HttpHeaderNames;
22  import io.netty.handler.codec.http.HttpHeaderValues;
23  import io.netty.handler.codec.http.HttpRequest;
24  import io.netty.util.internal.ObjectUtil;
25  import io.netty.util.internal.StringUtil;
26  
27  import java.nio.charset.Charset;
28  import java.util.List;
29  
30  /**
31   * This decoder will decode Body and can handle POST BODY.
32   *
33   * You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
34   *
35   */
36  public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
37  
38      static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024;
39  
40      static final int DEFAULT_MAX_FIELDS = 128;
41  
42      static final int DEFAULT_MAX_BUFFERED_BYTES = 1024;
43  
44      private final InterfaceHttpPostRequestDecoder decoder;
45  
46      /**
47       *
48       * @param request
49       *            the request to decode
50       * @throws NullPointerException
51       *             for request
52       * @throws ErrorDataDecoderException
53       *             if the default charset was wrong when decoding or other
54       *             errors
55       */
56      public HttpPostRequestDecoder(HttpRequest request) {
57          this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
58      }
59  
60      /**
61       *
62       * @param request
63       *            the request to decode
64       * @param maxFields
65       *            the maximum number of fields the form can have, {@code -1} to disable
66       * @param maxBufferedBytes
67       *            the maximum number of bytes the decoder can buffer when decoding a field, {@code -1} to disable
68       * @throws NullPointerException
69       *             for request
70       * @throws ErrorDataDecoderException
71       *             if the default charset was wrong when decoding or other
72       *             errors
73       */
74      public HttpPostRequestDecoder(HttpRequest request, int maxFields, int maxBufferedBytes) {
75          this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET,
76               maxFields, maxBufferedBytes);
77      }
78  
79      /**
80       *
81       * @param factory
82       *            the factory used to create InterfaceHttpData
83       * @param request
84       *            the request to decode
85       * @throws NullPointerException
86       *             for request or factory
87       * @throws ErrorDataDecoderException
88       *             if the default charset was wrong when decoding or other
89       *             errors
90       */
91      public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) {
92          this(factory, request, HttpConstants.DEFAULT_CHARSET);
93      }
94  
95      /**
96       *
97       * @param factory
98       *            the factory used to create InterfaceHttpData
99       * @param request
100      *            the request to decode
101      * @param charset
102      *            the charset to use as default
103      * @throws NullPointerException
104      *             for request or charset or factory
105      * @throws ErrorDataDecoderException
106      *             if the default charset was wrong when decoding or other
107      *             errors
108      */
109     public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
110         ObjectUtil.checkNotNull(factory, "factory");
111         ObjectUtil.checkNotNull(request, "request");
112         ObjectUtil.checkNotNull(charset, "charset");
113 
114         // Fill default values
115         if (isMultipart(request)) {
116             decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
117         } else {
118             decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
119         }
120     }
121 
122     /**
123      *
124      * @param factory
125      *            the factory used to create InterfaceHttpData
126      * @param request
127      *            the request to decode
128      * @param charset
129      *            the charset to use as default
130      * @param maxFields
131      *            the maximum number of fields the form can have, {@code -1} to disable
132      * @param maxBufferedBytes
133      *            the maximum number of bytes the decoder can buffer when decoding a field, {@code -1} to disable
134      * @throws NullPointerException
135      *             for request or charset or factory
136      * @throws ErrorDataDecoderException
137      *             if the default charset was wrong when decoding or other
138      *             errors
139      */
140     public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset,
141                                   int maxFields, int maxBufferedBytes) {
142         ObjectUtil.checkNotNull(factory, "factory");
143         ObjectUtil.checkNotNull(request, "request");
144         ObjectUtil.checkNotNull(charset, "charset");
145 
146         // Fill default values
147         if (isMultipart(request)) {
148             decoder = new HttpPostMultipartRequestDecoder(factory, request, charset, maxFields, maxBufferedBytes);
149         } else {
150             decoder = new HttpPostStandardRequestDecoder(factory, request, charset, maxFields, maxBufferedBytes);
151         }
152     }
153 
154     /**
155      * states follow NOTSTARTED PREAMBLE ( (HEADERDELIMITER DISPOSITION (FIELD |
156      * FILEUPLOAD))* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE (MIXEDDELIMITER
157      * MIXEDDISPOSITION MIXEDFILEUPLOAD)+ MIXEDCLOSEDELIMITER)* CLOSEDELIMITER)+
158      * EPILOGUE
159      *
160      * First getStatus is: NOSTARTED
161      *
162      * Content-type: multipart/form-data, boundary=AaB03x => PREAMBLE in Header
163      *
164      * --AaB03x => HEADERDELIMITER content-disposition: form-data; name="field1"
165      * => DISPOSITION
166      *
167      * Joe Blow => FIELD --AaB03x => HEADERDELIMITER content-disposition:
168      * form-data; name="pics" => DISPOSITION Content-type: multipart/mixed,
169      * boundary=BbC04y
170      *
171      * --BbC04y => MIXEDDELIMITER Content-disposition: attachment;
172      * filename="file1.txt" => MIXEDDISPOSITION Content-Type: text/plain
173      *
174      * ... contents of file1.txt ... => MIXEDFILEUPLOAD --BbC04y =>
175      * MIXEDDELIMITER Content-disposition: file; filename="file2.gif" =>
176      * MIXEDDISPOSITION Content-type: image/gif Content-Transfer-Encoding:
177      * binary
178      *
179      * ...contents of file2.gif... => MIXEDFILEUPLOAD --BbC04y-- =>
180      * MIXEDCLOSEDELIMITER --AaB03x-- => CLOSEDELIMITER
181      *
182      * Once CLOSEDELIMITER is found, last getStatus is EPILOGUE
183      */
184     protected enum MultiPartStatus {
185         NOTSTARTED, PREAMBLE, HEADERDELIMITER, DISPOSITION, FIELD, FILEUPLOAD, MIXEDPREAMBLE, MIXEDDELIMITER,
186         MIXEDDISPOSITION, MIXEDFILEUPLOAD, MIXEDCLOSEDELIMITER, CLOSEDELIMITER, PREEPILOGUE, EPILOGUE
187     }
188 
189     /**
190      * Check if the given request is a multipart request
191      * @return True if the request is a Multipart request
192      */
193     public static boolean isMultipart(HttpRequest request) {
194         String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
195         if (mimeType != null && mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {
196             return getMultipartDataBoundary(mimeType) != null;
197         }
198         return false;
199     }
200 
201     /**
202      * Check from the request ContentType if this request is a Multipart request.
203      * @return an array of String if multipartDataBoundary exists with the multipartDataBoundary
204      * as first element, charset if any as second (missing if not set), else null
205      */
206     protected static String[] getMultipartDataBoundary(String contentType) {
207         // Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]"
208         String[] headerContentType = splitHeaderContentType(contentType);
209         final String multiPartHeader = HttpHeaderValues.MULTIPART_FORM_DATA.toString();
210         if (headerContentType[0].regionMatches(true, 0, multiPartHeader, 0 , multiPartHeader.length())) {
211             int mrank;
212             int crank;
213             final String boundaryHeader = HttpHeaderValues.BOUNDARY.toString();
214             if (headerContentType[1].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) {
215                 mrank = 1;
216                 crank = 2;
217             } else if (headerContentType[2].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) {
218                 mrank = 2;
219                 crank = 1;
220             } else {
221                 return null;
222             }
223             String boundary = StringUtil.substringAfter(headerContentType[mrank], '=');
224             if (boundary == null) {
225                 throw new ErrorDataDecoderException("Needs a boundary value");
226             }
227             if (boundary.charAt(0) == '"') {
228                 String bound = boundary.trim();
229                 int index = bound.length() - 1;
230                 if (bound.charAt(index) == '"') {
231                     boundary = bound.substring(1, index);
232                 }
233             }
234             final String charsetHeader = HttpHeaderValues.CHARSET.toString();
235             if (headerContentType[crank].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {
236                 String charset = StringUtil.substringAfter(headerContentType[crank], '=');
237                 if (charset != null) {
238                     return new String[] {"--" + boundary, charset};
239                 }
240             }
241             return new String[] {"--" + boundary};
242         }
243         return null;
244     }
245 
246     @Override
247     public boolean isMultipart() {
248         return decoder.isMultipart();
249     }
250 
251     @Override
252     public void setDiscardThreshold(int discardThreshold) {
253         decoder.setDiscardThreshold(discardThreshold);
254     }
255 
256     @Override
257     public int getDiscardThreshold() {
258         return decoder.getDiscardThreshold();
259     }
260 
261     @Override
262     public List<InterfaceHttpData> getBodyHttpDatas() {
263         return decoder.getBodyHttpDatas();
264     }
265 
266     @Override
267     public List<InterfaceHttpData> getBodyHttpDatas(String name) {
268         return decoder.getBodyHttpDatas(name);
269     }
270 
271     @Override
272     public InterfaceHttpData getBodyHttpData(String name) {
273         return decoder.getBodyHttpData(name);
274     }
275 
276     @Override
277     public InterfaceHttpPostRequestDecoder offer(HttpContent content) {
278         return decoder.offer(content);
279     }
280 
281     @Override
282     public boolean hasNext() {
283         return decoder.hasNext();
284     }
285 
286     @Override
287     public InterfaceHttpData next() {
288         return decoder.next();
289     }
290 
291     @Override
292     public InterfaceHttpData currentPartialHttpData() {
293         return decoder.currentPartialHttpData();
294     }
295 
296     @Override
297     public void destroy() {
298         decoder.destroy();
299     }
300 
301     @Override
302     public void cleanFiles() {
303         decoder.cleanFiles();
304     }
305 
306     @Override
307     public void removeHttpDataFromClean(InterfaceHttpData data) {
308         decoder.removeHttpDataFromClean(data);
309     }
310 
311     /**
312      * Split the very first line (Content-Type value) in 3 Strings
313      *
314      * @return the array of 3 Strings
315      */
316     private static String[] splitHeaderContentType(String sb) {
317         int aStart;
318         int aEnd;
319         int bStart;
320         int bEnd;
321         int cStart;
322         int cEnd;
323         aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
324         aEnd =  sb.indexOf(';');
325         if (aEnd == -1) {
326             return new String[] { sb, "", "" };
327         }
328         bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);
329         if (sb.charAt(aEnd - 1) == ' ') {
330             aEnd--;
331         }
332         bEnd =  sb.indexOf(';', bStart);
333         if (bEnd == -1) {
334             bEnd = HttpPostBodyUtil.findEndOfString(sb);
335             return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), "" };
336         }
337         cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);
338         if (sb.charAt(bEnd - 1) == ' ') {
339             bEnd--;
340         }
341         cEnd = HttpPostBodyUtil.findEndOfString(sb);
342         return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), sb.substring(cStart, cEnd) };
343     }
344 
345     /**
346      * Exception when try reading data from request in chunked format, and not
347      * enough data are available (need more chunks)
348      */
349     public static class NotEnoughDataDecoderException extends DecoderException {
350         private static final long serialVersionUID = -7846841864603865638L;
351 
352         public NotEnoughDataDecoderException() {
353         }
354 
355         public NotEnoughDataDecoderException(String msg) {
356             super(msg);
357         }
358 
359         public NotEnoughDataDecoderException(Throwable cause) {
360             super(cause);
361         }
362 
363         public NotEnoughDataDecoderException(String msg, Throwable cause) {
364             super(msg, cause);
365         }
366     }
367 
368     /**
369      * Exception when the body is fully decoded, even if there is still data
370      */
371     public static class EndOfDataDecoderException extends DecoderException {
372         private static final long serialVersionUID = 1336267941020800769L;
373     }
374 
375     /**
376      * Exception when an error occurs while decoding
377      */
378     public static class ErrorDataDecoderException extends DecoderException {
379         private static final long serialVersionUID = 5020247425493164465L;
380 
381         public ErrorDataDecoderException() {
382         }
383 
384         public ErrorDataDecoderException(String msg) {
385             super(msg);
386         }
387 
388         public ErrorDataDecoderException(Throwable cause) {
389             super(cause);
390         }
391 
392         public ErrorDataDecoderException(String msg, Throwable cause) {
393             super(msg, cause);
394         }
395     }
396 
397     /**
398      * Exception when the maximum number of fields for a given form is reached
399      */
400     public static final class TooManyFormFieldsException extends DecoderException {
401         private static final long serialVersionUID = 1336267941020800769L;
402     }
403 
404     /**
405      * Exception when a field content is too long
406      */
407     public static final class TooLongFormFieldException extends DecoderException {
408         private static final long serialVersionUID = 1336267941020800769L;
409     }
410 }