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