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