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