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().getAndConvert(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         if (headerContentType[0].toLowerCase().startsWith(
159                 HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {
160             int mrank;
161             int crank;
162             if (headerContentType[1].toLowerCase().startsWith(
163                     HttpHeaderValues.BOUNDARY.toString())) {
164                 mrank = 1;
165                 crank = 2;
166             } else if (headerContentType[2].toLowerCase().startsWith(
167                     HttpHeaderValues.BOUNDARY.toString())) {
168                 mrank = 2;
169                 crank = 1;
170             } else {
171                 return null;
172             }
173             String boundary = StringUtil.substringAfter(headerContentType[mrank], '=');
174             if (boundary == null) {
175                 throw new ErrorDataDecoderException("Needs a boundary value");
176             }
177             if (boundary.charAt(0) == '"') {
178                 String bound = boundary.trim();
179                 int index = bound.length() - 1;
180                 if (bound.charAt(index) == '"') {
181                     boundary = bound.substring(1, index);
182                 }
183             }
184             if (headerContentType[crank].toLowerCase().startsWith(
185                     HttpHeaderValues.CHARSET.toString())) {
186                 String charset = StringUtil.substringAfter(headerContentType[crank], '=');
187                 if (charset != null) {
188                     return new String[] {"--" + boundary, charset};
189                 }
190             }
191             return new String[] {"--" + boundary};
192         }
193         return null;
194     }
195 
196     @Override
197     public boolean isMultipart() {
198         return decoder.isMultipart();
199     }
200 
201     @Override
202     public void setDiscardThreshold(int discardThreshold) {
203         decoder.setDiscardThreshold(discardThreshold);
204     }
205 
206     @Override
207     public int getDiscardThreshold() {
208         return decoder.getDiscardThreshold();
209     }
210 
211     @Override
212     public List<InterfaceHttpData> getBodyHttpDatas() {
213         return decoder.getBodyHttpDatas();
214     }
215 
216     @Override
217     public List<InterfaceHttpData> getBodyHttpDatas(String name) {
218         return decoder.getBodyHttpDatas(name);
219     }
220 
221     @Override
222     public InterfaceHttpData getBodyHttpData(String name) {
223         return decoder.getBodyHttpData(name);
224     }
225 
226     @Override
227     public InterfaceHttpPostRequestDecoder offer(HttpContent content) {
228         return decoder.offer(content);
229     }
230 
231     @Override
232     public boolean hasNext() {
233         return decoder.hasNext();
234     }
235 
236     @Override
237     public InterfaceHttpData next() {
238         return decoder.next();
239     }
240 
241     @Override
242     public void destroy() {
243         decoder.destroy();
244     }
245 
246     @Override
247     public void cleanFiles() {
248         decoder.cleanFiles();
249     }
250 
251     @Override
252     public void removeHttpDataFromClean(InterfaceHttpData data) {
253         decoder.removeHttpDataFromClean(data);
254     }
255 
256     /**
257      * Split the very first line (Content-Type value) in 3 Strings
258      *
259      * @return the array of 3 Strings
260      */
261     private static String[] splitHeaderContentType(String sb) {
262         int aStart;
263         int aEnd;
264         int bStart;
265         int bEnd;
266         int cStart;
267         int cEnd;
268         aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
269         aEnd =  sb.indexOf(';');
270         if (aEnd == -1) {
271             return new String[] { sb, "", "" };
272         }
273         bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);
274         if (sb.charAt(aEnd - 1) == ' ') {
275             aEnd--;
276         }
277         bEnd =  sb.indexOf(';', bStart);
278         if (bEnd == -1) {
279             bEnd = HttpPostBodyUtil.findEndOfString(sb);
280             return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), "" };
281         }
282         cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);
283         if (sb.charAt(bEnd - 1) == ' ') {
284             bEnd--;
285         }
286         cEnd = HttpPostBodyUtil.findEndOfString(sb);
287         return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), sb.substring(cStart, cEnd) };
288     }
289 
290     /**
291      * Exception when try reading data from request in chunked format, and not
292      * enough data are available (need more chunks)
293      */
294     public static class NotEnoughDataDecoderException extends DecoderException {
295         private static final long serialVersionUID = -7846841864603865638L;
296 
297         public NotEnoughDataDecoderException() {
298         }
299 
300         public NotEnoughDataDecoderException(String msg) {
301             super(msg);
302         }
303 
304         public NotEnoughDataDecoderException(Throwable cause) {
305             super(cause);
306         }
307 
308         public NotEnoughDataDecoderException(String msg, Throwable cause) {
309             super(msg, cause);
310         }
311     }
312 
313     /**
314      * Exception when the body is fully decoded, even if there is still data
315      */
316     public static class EndOfDataDecoderException extends DecoderException {
317         private static final long serialVersionUID = 1336267941020800769L;
318     }
319 
320     /**
321      * Exception when an error occurs while decoding
322      */
323     public static class ErrorDataDecoderException extends DecoderException {
324         private static final long serialVersionUID = 5020247425493164465L;
325 
326         public ErrorDataDecoderException() {
327         }
328 
329         public ErrorDataDecoderException(String msg) {
330             super(msg);
331         }
332 
333         public ErrorDataDecoderException(Throwable cause) {
334             super(cause);
335         }
336 
337         public ErrorDataDecoderException(String msg, Throwable cause) {
338             super(msg, cause);
339         }
340     }
341 }